We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Errors while interacting with resources in a lesson on Learn-only device

## Observed behavior
Replicable on 0.15.0b3 installed on both Windows 7 & 10 with learn-only device on Android 9.0 phone. Errors appear in seemingly random fashion on different resources at different moments. Learner device appears to be syncing correctly with the server, and progress (when achieved) is registered on Coach page.

1 | 2 | 3
--- | --- | ---
![DevTools - localhost:8080-es-es-learn---topics-c-d753aaf3ac794cf59b0169df72964dbe?lessonId=b44f7ef78626b7041476861b64f1c7f1 classId=1b21fb43599f223b4885e95b61aa2ef3 last=LESSON_PLAYLIST_002](https://user-images.githubusercontent.com/1457929/145158636-00e2e472-028d-471a-8c64-1bf2ef039414.png) | ![DevTools - localhost:8080-pt-mz-learn---home_001](https://user-images.githubusercontent.com/1457929/145158660-b5b360d9-b46c-4280-9128-292ed7969670.png) | ![DevTools - localhost:8080-fr-fr-learn-_001](https://user-images.githubusercontent.com/1457929/145158678-3c095b86-9f8b-45a9-b13b-5c859dccef11.png)


## Errors and logs

```
{
  "data": "<h1>Server Error (500)</h1>",
  "status": 500,
  "statusText": "Internal Server Error",
  "headers": {
    "content-length": "27",
    "content-type": "text/html",
    "date": "Wed, 08 Dec 2021 01:20:57 GMT",
    "server": "Cheroot/unknown",
    "vary": "Cookie",
    "x-frame-options": "SAMEORIGIN"
  },
  "config": {
    "url": "/api/logger/trackprogress/c05a4300939687321e2195d8972941f7/",
    "method": "put",
    "data": "{\"time_spent_delta\":123.604}",
    "headers": {
      "Accept": "application/json, text/plain, */*",
      "Content-Type": "application/json;charset=utf-8",
      "X-Requested-With": "XMLHttpRequest",
      "X-CSRFToken": "WKza1d9ZwO52LYZNL8Ve0aBq1GsoKKio2ISgQZA2cHI4mNhLu0Ux5LlCG3ZfpCpd"
    },
    "transformRequest": [
      null
    ],
    "transformResponse": [
      null
    ],
    "timeout": 0,
    "xsrfCookieName": "kolibri_csrftoken",
    "xsrfHeaderName": "X-CSRFToken",
    "maxContentLength": -1
  },
  "request": {}
}
```

[db-and-logs.zip](https://github.com/learningequality/kolibri/files/7673793/db-and-logs.zip)

[full home folder](https://drive.google.com/file/d/1qgC6Ovfm91RzdgoFA4TEJNuWeZi-NS5L/view?usp=sharing) (Windows 7, 370MB)

## Expected behavior
...

## User-facing consequences
...

## Steps to reproduce
…

## Context
 * Kolibri version: 0.15.0b3
 * Operating system:  Windows 7 & 10 with learn-only device on Android 9.0 phone
 * Browser: Firefox

cc @marcellamaki @rtibbles 

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `kolibri/core/device/middleware.py`
Content:
```
1 from django.conf import settings
2 from django.db import OperationalError
3 from django.http import HttpResponse
4 from django.http import HttpResponseRedirect
5 from django.shortcuts import redirect
6 from django.urls import is_valid_path
7 from django.utils import translation
8 
9 from .translation import get_language_from_request_and_is_from_path
10 from kolibri.core.device.hooks import SetupHook
11 from kolibri.core.device.utils import DeviceNotProvisioned
12 from kolibri.utils.conf import OPTIONS
13 
14 
15 class KolibriLocaleMiddleware(object):
16     """
17     Copied and then modified into a new style middleware from:
18     https://github.com/django/django/blob/stable/1.11.x/django/middleware/locale.py
19     Also has several other changes to suit our purposes.
20     The principal concern of this middleware is to activate translation for the current
21     language, so that throughout the lifecycle of this request, any translation or language
22     related functionality is set to the appropriate locale.
23     Unlike the Django middleware, this middleware only runs on requests to URLs that are
24     prefixed by a language code. Other URLs, such as for untranslated API endpoints do not
25     have a language code set on them.
26     """
27 
28     def __init__(self, get_response):
29         # Standard boilerplate for a new style Django middleware.
30         self.get_response = get_response
31 
32     def __call__(self, request):
33         # First get the language code, and whether this was calculated from the path
34         # i.e. was this a language-prefixed URL.
35         language, language_from_path = get_language_from_request_and_is_from_path(
36             request
37         )
38         # If this URL has been resolved to a view, and the view is not on a language prefixed
39         # URL, then the function above will return None for the language code to indicate that
40         # no translation is necessary.
41         if language is not None:
42             # Only activate translation if there is a language code returned.
43             translation.activate(language)
44             request.LANGUAGE_CODE = translation.get_language()
45 
46         response = self.get_response(request)
47 
48         if language is not None:
49 
50             language = translation.get_language()
51 
52             if response.status_code == 404 and not language_from_path:
53                 # Maybe the language code is missing in the URL? Try adding the
54                 # language prefix and redirecting to that URL.
55                 # First get any global prefix that is being used.
56                 script_prefix = OPTIONS["Deployment"]["URL_PATH_PREFIX"]
57                 # Replace the global prefix with the global prefix and the language prefix.
58                 language_path = request.path_info.replace(
59                     script_prefix, "%s%s/" % (script_prefix, language), 1
60                 )
61 
62                 # Get the urlconf from the request, default to the global settings ROOT_URLCONF
63                 urlconf = getattr(request, "urlconf", settings.ROOT_URLCONF)
64                 # Check if this is a valid path
65                 path_valid = is_valid_path(language_path, urlconf)
66                 # Check if the path is only invalid because it is missing a trailing slash
67                 path_needs_slash = not path_valid and (
68                     settings.APPEND_SLASH
69                     and not language_path.endswith("/")
70                     and is_valid_path("%s/" % language_path, urlconf)
71                 )
72                 # If the constructed path is valid, or it would be valid with a trailing slash
73                 # then redirect to the prefixed path, with a trailing slash added if needed.
74                 if path_valid or path_needs_slash:
75                     # Insert language after the script prefix and before the
76                     # rest of the URL
77                     language_url = request.get_full_path(
78                         force_append_slash=path_needs_slash
79                     ).replace(script_prefix, "%s%s/" % (script_prefix, language), 1)
80                     return HttpResponseRedirect(language_url)
81 
82             # Add a content language header to the response if not already present.
83             if "Content-Language" not in response:
84                 response["Content-Language"] = language
85 
86         return response
87 
88 
89 class ProvisioningErrorHandler(object):
90     def __init__(self, get_response):
91         self.get_response = get_response
92 
93     def process_exception(self, request, exception):
94         if (
95             isinstance(exception, DeviceNotProvisioned)
96             and SetupHook.provision_url()
97             and not request.path.startswith(SetupHook.provision_url())
98         ):
99             return redirect(SetupHook.provision_url())
100         return None
101 
102     def __call__(self, request):
103         return self.get_response(request)
104 
105 
106 class DatabaseBusyErrorHandler(object):
107     """
108     A middleware class to raise a 503 when the database is under heavy load
109     For SQLite this will trigger for database locked errors.
110     For Postgres this will trigger for deadlocks.
111     """
112 
113     def __init__(self, get_response):
114         self.get_response = get_response
115 
116     def process_exception(self, request, exception):
117         if not isinstance(exception, OperationalError):
118             return None
119         if (
120             OPTIONS["Database"]["DATABASE_ENGINE"] == "sqlite"
121             and "database is locked" not in exception.args[0]
122         ):
123             return None
124         if (
125             OPTIONS["Database"]["DATABASE_ENGINE"] == "postgres"
126             and "deadlock detected" not in exception.args[0]
127         ):
128             return None
129         # Return a 503 response with a Retry-After of 10 seconds. In future we may be able to customize this value
130         # based on what is currently happening on the server.
131         return HttpResponse(
132             "Database is not available for write operations",
133             status=503,
134             headers={"Retry-After": 10},
135         )
136 
137     def __call__(self, request):
138         return self.get_response(request)
139
```
--- 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/kolibri/core/device/middleware.py b/kolibri/core/device/middleware.py
--- a/kolibri/core/device/middleware.py
+++ b/kolibri/core/device/middleware.py
@@ -128,11 +128,12 @@
             return None
         # Return a 503 response with a Retry-After of 10 seconds. In future we may be able to customize this value
         # based on what is currently happening on the server.
-        return HttpResponse(
+        response = HttpResponse(
             "Database is not available for write operations",
             status=503,
-            headers={"Retry-After": 10},
         )
+        response["Retry-After"] = 10
+        return response
 
     def __call__(self, request):
         return self.get_response(request)

{"golden_diff": "diff --git a/kolibri/core/device/middleware.py b/kolibri/core/device/middleware.py\n--- a/kolibri/core/device/middleware.py\n+++ b/kolibri/core/device/middleware.py\n@@ -128,11 +128,12 @@\n             return None\n         # Return a 503 response with a Retry-After of 10 seconds. In future we may be able to customize this value\n         # based on what is currently happening on the server.\n-        return HttpResponse(\n+        response = HttpResponse(\n             \"Database is not available for write operations\",\n             status=503,\n-            headers={\"Retry-After\": 10},\n         )\n+        response[\"Retry-After\"] = 10\n+        return response\n \n     def __call__(self, request):\n         return self.get_response(request)\n", "issue": "Errors while interacting with resources in a lesson on Learn-only device\n\r\n## Observed behavior\r\nReplicable on 0.15.0b3 installed on both Windows 7 & 10 with learn-only device on Android 9.0 phone. Errors appear in seemingly random fashion on different resources at different moments. Learner device appears to be syncing correctly with the server, and progress (when achieved) is registered on Coach page.\r\n\r\n1 | 2 | 3\r\n--- | --- | ---\r\n![DevTools - localhost:8080-es-es-learn---topics-c-d753aaf3ac794cf59b0169df72964dbe?lessonId=b44f7ef78626b7041476861b64f1c7f1 classId=1b21fb43599f223b4885e95b61aa2ef3 last=LESSON_PLAYLIST_002](https://user-images.githubusercontent.com/1457929/145158636-00e2e472-028d-471a-8c64-1bf2ef039414.png) | ![DevTools - localhost:8080-pt-mz-learn---home_001](https://user-images.githubusercontent.com/1457929/145158660-b5b360d9-b46c-4280-9128-292ed7969670.png) | ![DevTools - localhost:8080-fr-fr-learn-_001](https://user-images.githubusercontent.com/1457929/145158678-3c095b86-9f8b-45a9-b13b-5c859dccef11.png)\r\n\r\n\r\n## Errors and logs\r\n\r\n```\r\n{\r\n  \"data\": \"<h1>Server Error (500)</h1>\",\r\n  \"status\": 500,\r\n  \"statusText\": \"Internal Server Error\",\r\n  \"headers\": {\r\n    \"content-length\": \"27\",\r\n    \"content-type\": \"text/html\",\r\n    \"date\": \"Wed, 08 Dec 2021 01:20:57 GMT\",\r\n    \"server\": \"Cheroot/unknown\",\r\n    \"vary\": \"Cookie\",\r\n    \"x-frame-options\": \"SAMEORIGIN\"\r\n  },\r\n  \"config\": {\r\n    \"url\": \"/api/logger/trackprogress/c05a4300939687321e2195d8972941f7/\",\r\n    \"method\": \"put\",\r\n    \"data\": \"{\\\"time_spent_delta\\\":123.604}\",\r\n    \"headers\": {\r\n      \"Accept\": \"application/json, text/plain, */*\",\r\n      \"Content-Type\": \"application/json;charset=utf-8\",\r\n      \"X-Requested-With\": \"XMLHttpRequest\",\r\n      \"X-CSRFToken\": \"WKza1d9ZwO52LYZNL8Ve0aBq1GsoKKio2ISgQZA2cHI4mNhLu0Ux5LlCG3ZfpCpd\"\r\n    },\r\n    \"transformRequest\": [\r\n      null\r\n    ],\r\n    \"transformResponse\": [\r\n      null\r\n    ],\r\n    \"timeout\": 0,\r\n    \"xsrfCookieName\": \"kolibri_csrftoken\",\r\n    \"xsrfHeaderName\": \"X-CSRFToken\",\r\n    \"maxContentLength\": -1\r\n  },\r\n  \"request\": {}\r\n}\r\n```\r\n\r\n[db-and-logs.zip](https://github.com/learningequality/kolibri/files/7673793/db-and-logs.zip)\r\n\r\n[full home folder](https://drive.google.com/file/d/1qgC6Ovfm91RzdgoFA4TEJNuWeZi-NS5L/view?usp=sharing) (Windows 7, 370MB)\r\n\r\n## Expected behavior\r\n...\r\n\r\n## User-facing consequences\r\n...\r\n\r\n## Steps to reproduce\r\n\u2026\r\n\r\n## Context\r\n * Kolibri version: 0.15.0b3\r\n * Operating system:  Windows 7 & 10 with learn-only device on Android 9.0 phone\r\n * Browser: Firefox\r\n\r\ncc @marcellamaki @rtibbles \n", "before_files": [{"content": "from django.conf import settings\nfrom django.db import OperationalError\nfrom django.http import HttpResponse\nfrom django.http import HttpResponseRedirect\nfrom django.shortcuts import redirect\nfrom django.urls import is_valid_path\nfrom django.utils import translation\n\nfrom .translation import get_language_from_request_and_is_from_path\nfrom kolibri.core.device.hooks import SetupHook\nfrom kolibri.core.device.utils import DeviceNotProvisioned\nfrom kolibri.utils.conf import OPTIONS\n\n\nclass KolibriLocaleMiddleware(object):\n    \"\"\"\n    Copied and then modified into a new style middleware from:\n    https://github.com/django/django/blob/stable/1.11.x/django/middleware/locale.py\n    Also has several other changes to suit our purposes.\n    The principal concern of this middleware is to activate translation for the current\n    language, so that throughout the lifecycle of this request, any translation or language\n    related functionality is set to the appropriate locale.\n    Unlike the Django middleware, this middleware only runs on requests to URLs that are\n    prefixed by a language code. Other URLs, such as for untranslated API endpoints do not\n    have a language code set on them.\n    \"\"\"\n\n    def __init__(self, get_response):\n        # Standard boilerplate for a new style Django middleware.\n        self.get_response = get_response\n\n    def __call__(self, request):\n        # First get the language code, and whether this was calculated from the path\n        # i.e. was this a language-prefixed URL.\n        language, language_from_path = get_language_from_request_and_is_from_path(\n            request\n        )\n        # If this URL has been resolved to a view, and the view is not on a language prefixed\n        # URL, then the function above will return None for the language code to indicate that\n        # no translation is necessary.\n        if language is not None:\n            # Only activate translation if there is a language code returned.\n            translation.activate(language)\n            request.LANGUAGE_CODE = translation.get_language()\n\n        response = self.get_response(request)\n\n        if language is not None:\n\n            language = translation.get_language()\n\n            if response.status_code == 404 and not language_from_path:\n                # Maybe the language code is missing in the URL? Try adding the\n                # language prefix and redirecting to that URL.\n                # First get any global prefix that is being used.\n                script_prefix = OPTIONS[\"Deployment\"][\"URL_PATH_PREFIX\"]\n                # Replace the global prefix with the global prefix and the language prefix.\n                language_path = request.path_info.replace(\n                    script_prefix, \"%s%s/\" % (script_prefix, language), 1\n                )\n\n                # Get the urlconf from the request, default to the global settings ROOT_URLCONF\n                urlconf = getattr(request, \"urlconf\", settings.ROOT_URLCONF)\n                # Check if this is a valid path\n                path_valid = is_valid_path(language_path, urlconf)\n                # Check if the path is only invalid because it is missing a trailing slash\n                path_needs_slash = not path_valid and (\n                    settings.APPEND_SLASH\n                    and not language_path.endswith(\"/\")\n                    and is_valid_path(\"%s/\" % language_path, urlconf)\n                )\n                # If the constructed path is valid, or it would be valid with a trailing slash\n                # then redirect to the prefixed path, with a trailing slash added if needed.\n                if path_valid or path_needs_slash:\n                    # Insert language after the script prefix and before the\n                    # rest of the URL\n                    language_url = request.get_full_path(\n                        force_append_slash=path_needs_slash\n                    ).replace(script_prefix, \"%s%s/\" % (script_prefix, language), 1)\n                    return HttpResponseRedirect(language_url)\n\n            # Add a content language header to the response if not already present.\n            if \"Content-Language\" not in response:\n                response[\"Content-Language\"] = language\n\n        return response\n\n\nclass ProvisioningErrorHandler(object):\n    def __init__(self, get_response):\n        self.get_response = get_response\n\n    def process_exception(self, request, exception):\n        if (\n            isinstance(exception, DeviceNotProvisioned)\n            and SetupHook.provision_url()\n            and not request.path.startswith(SetupHook.provision_url())\n        ):\n            return redirect(SetupHook.provision_url())\n        return None\n\n    def __call__(self, request):\n        return self.get_response(request)\n\n\nclass DatabaseBusyErrorHandler(object):\n    \"\"\"\n    A middleware class to raise a 503 when the database is under heavy load\n    For SQLite this will trigger for database locked errors.\n    For Postgres this will trigger for deadlocks.\n    \"\"\"\n\n    def __init__(self, get_response):\n        self.get_response = get_response\n\n    def process_exception(self, request, exception):\n        if not isinstance(exception, OperationalError):\n            return None\n        if (\n            OPTIONS[\"Database\"][\"DATABASE_ENGINE\"] == \"sqlite\"\n            and \"database is locked\" not in exception.args[0]\n        ):\n            return None\n        if (\n            OPTIONS[\"Database\"][\"DATABASE_ENGINE\"] == \"postgres\"\n            and \"deadlock detected\" not in exception.args[0]\n        ):\n            return None\n        # Return a 503 response with a Retry-After of 10 seconds. In future we may be able to customize this value\n        # based on what is currently happening on the server.\n        return HttpResponse(\n            \"Database is not available for write operations\",\n            status=503,\n            headers={\"Retry-After\": 10},\n        )\n\n    def __call__(self, request):\n        return self.get_response(request)\n", "path": "kolibri/core/device/middleware.py"}], "after_files": [{"content": "from django.conf import settings\nfrom django.db import OperationalError\nfrom django.http import HttpResponse\nfrom django.http import HttpResponseRedirect\nfrom django.shortcuts import redirect\nfrom django.urls import is_valid_path\nfrom django.utils import translation\n\nfrom .translation import get_language_from_request_and_is_from_path\nfrom kolibri.core.device.hooks import SetupHook\nfrom kolibri.core.device.utils import DeviceNotProvisioned\nfrom kolibri.utils.conf import OPTIONS\n\n\nclass KolibriLocaleMiddleware(object):\n    \"\"\"\n    Copied and then modified into a new style middleware from:\n    https://github.com/django/django/blob/stable/1.11.x/django/middleware/locale.py\n    Also has several other changes to suit our purposes.\n    The principal concern of this middleware is to activate translation for the current\n    language, so that throughout the lifecycle of this request, any translation or language\n    related functionality is set to the appropriate locale.\n    Unlike the Django middleware, this middleware only runs on requests to URLs that are\n    prefixed by a language code. Other URLs, such as for untranslated API endpoints do not\n    have a language code set on them.\n    \"\"\"\n\n    def __init__(self, get_response):\n        # Standard boilerplate for a new style Django middleware.\n        self.get_response = get_response\n\n    def __call__(self, request):\n        # First get the language code, and whether this was calculated from the path\n        # i.e. was this a language-prefixed URL.\n        language, language_from_path = get_language_from_request_and_is_from_path(\n            request\n        )\n        # If this URL has been resolved to a view, and the view is not on a language prefixed\n        # URL, then the function above will return None for the language code to indicate that\n        # no translation is necessary.\n        if language is not None:\n            # Only activate translation if there is a language code returned.\n            translation.activate(language)\n            request.LANGUAGE_CODE = translation.get_language()\n\n        response = self.get_response(request)\n\n        if language is not None:\n\n            language = translation.get_language()\n\n            if response.status_code == 404 and not language_from_path:\n                # Maybe the language code is missing in the URL? Try adding the\n                # language prefix and redirecting to that URL.\n                # First get any global prefix that is being used.\n                script_prefix = OPTIONS[\"Deployment\"][\"URL_PATH_PREFIX\"]\n                # Replace the global prefix with the global prefix and the language prefix.\n                language_path = request.path_info.replace(\n                    script_prefix, \"%s%s/\" % (script_prefix, language), 1\n                )\n\n                # Get the urlconf from the request, default to the global settings ROOT_URLCONF\n                urlconf = getattr(request, \"urlconf\", settings.ROOT_URLCONF)\n                # Check if this is a valid path\n                path_valid = is_valid_path(language_path, urlconf)\n                # Check if the path is only invalid because it is missing a trailing slash\n                path_needs_slash = not path_valid and (\n                    settings.APPEND_SLASH\n                    and not language_path.endswith(\"/\")\n                    and is_valid_path(\"%s/\" % language_path, urlconf)\n                )\n                # If the constructed path is valid, or it would be valid with a trailing slash\n                # then redirect to the prefixed path, with a trailing slash added if needed.\n                if path_valid or path_needs_slash:\n                    # Insert language after the script prefix and before the\n                    # rest of the URL\n                    language_url = request.get_full_path(\n                        force_append_slash=path_needs_slash\n                    ).replace(script_prefix, \"%s%s/\" % (script_prefix, language), 1)\n                    return HttpResponseRedirect(language_url)\n\n            # Add a content language header to the response if not already present.\n            if \"Content-Language\" not in response:\n                response[\"Content-Language\"] = language\n\n        return response\n\n\nclass ProvisioningErrorHandler(object):\n    def __init__(self, get_response):\n        self.get_response = get_response\n\n    def process_exception(self, request, exception):\n        if (\n            isinstance(exception, DeviceNotProvisioned)\n            and SetupHook.provision_url()\n            and not request.path.startswith(SetupHook.provision_url())\n        ):\n            return redirect(SetupHook.provision_url())\n        return None\n\n    def __call__(self, request):\n        return self.get_response(request)\n\n\nclass DatabaseBusyErrorHandler(object):\n    \"\"\"\n    A middleware class to raise a 503 when the database is under heavy load\n    For SQLite this will trigger for database locked errors.\n    For Postgres this will trigger for deadlocks.\n    \"\"\"\n\n    def __init__(self, get_response):\n        self.get_response = get_response\n\n    def process_exception(self, request, exception):\n        if not isinstance(exception, OperationalError):\n            return None\n        if (\n            OPTIONS[\"Database\"][\"DATABASE_ENGINE\"] == \"sqlite\"\n            and \"database is locked\" not in exception.args[0]\n        ):\n            return None\n        if (\n            OPTIONS[\"Database\"][\"DATABASE_ENGINE\"] == \"postgres\"\n            and \"deadlock detected\" not in exception.args[0]\n        ):\n            return None\n        # Return a 503 response with a Retry-After of 10 seconds. In future we may be able to customize this value\n        # based on what is currently happening on the server.\n        response = HttpResponse(\n            \"Database is not available for write operations\",\n            status=503,\n        )\n        response[\"Retry-After\"] = 10\n        return response\n\n    def __call__(self, request):\n        return self.get_response(request)\n", "path": "kolibri/core/device/middleware.py"}]}