We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Sanic 21.9.2 breaks exception tracking
For yet unknown reasons, Sanic 21.9.2+ broke exception tracking. The changes between 21.9.1 and 21.9.2 are here:

https://github.com/sanic-org/sanic/compare/v21.9.1...v21.9.2

The test failures are here: https://apm-ci.elastic.co/blue/organizations/jenkins/apm-agent-python%2Fapm-agent-python-nightly-mbp%2Fmaster/detail/master/787/tests/

Example:

    ----------------------------------------------------- Captured log call ------------------------------------------------------INFO     sanic.root:testing.py:82 http://127.0.0.1:50003/fallback-value-error
    ERROR    sanic.error:request.py:193 Exception occurred in one of response middleware handlers
    Traceback (most recent call last):
    File "/home/user/.local/lib/python3.10/site-packages/sanic_routing/router.py", line 79, in resolve
        route, param_basket = self.find_route(
    File "", line 24, in find_route
    sanic_routing.exceptions.NotFound: Not Found

    During handling of the above exception, another exception occurred:

    Traceback (most recent call last):
    File "/home/user/.local/lib/python3.10/site-packages/sanic/router.py", line 38, in _get
        return self.resolve(
    File "/home/user/.local/lib/python3.10/site-packages/sanic_routing/router.py", line 96, in resolve
        raise self.exception(str(e), path=path)
    sanic_routing.exceptions.NotFound: Not Found

    During handling of the above exception, another exception occurred:

    Traceback (most recent call last):
    File "handle_request", line 26, in handle_request
        Any,
    File "/home/user/.local/lib/python3.10/site-packages/sanic/router.py", line 66, in get
        return self._get(path, method, host)
    File "/home/user/.local/lib/python3.10/site-packages/sanic/router.py", line 44, in _get
        raise NotFound("Requested URL {} not found".format(e.path))
    sanic.exceptions.NotFound: Requested URL /fallback-value-error not found

    During handling of the above exception, another exception occurred:

    Traceback (most recent call last):
    File "/home/user/.local/lib/python3.10/site-packages/sanic/request.py", line 187, in respond
        response = await self.app._run_response_middleware(
    File "_run_response_middleware", line 22, in _run_response_middleware
        from ssl import Purpose, SSLContext, create_default_context
    File "/app/elasticapm/contrib/sanic/__init__.py", line 279, in _instrument_response
        await set_context(
    File "/app/elasticapm/contrib/asyncio/traces.py", line 93, in set_context
        data = await data()
    File "/app/elasticapm/contrib/sanic/utils.py", line 121, in get_response_info
        if config.capture_body and "octet-stream" not in response.content_type:
    TypeError: argument of type 'NoneType' is not iterable

Checking for `response.content_type is not None` in `elasticapm/contrib/sanic/utils.py:121` doesn't resolve the issue.

@ahopkins do you happen to have an idea what could cause these failures?

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `elasticapm/contrib/sanic/utils.py`
Content:
```
1 #  BSD 3-Clause License
2 #
3 #  Copyright (c) 2012, the Sentry Team, see AUTHORS for more details
4 #  Copyright (c) 2019, Elasticsearch BV
5 #  All rights reserved.
6 #
7 #  Redistribution and use in source and binary forms, with or without
8 #  modification, are permitted provided that the following conditions are met:
9 #
10 #  * Redistributions of source code must retain the above copyright notice, this
11 #    list of conditions and the following disclaimer.
12 #
13 #  * Redistributions in binary form must reproduce the above copyright notice,
14 #    this list of conditions and the following disclaimer in the documentation
15 #    and/or other materials provided with the distribution.
16 #
17 #  * Neither the name of the copyright holder nor the names of its
18 #    contributors may be used to endorse or promote products derived from
19 #    this software without specific prior written permission.
20 #
21 #  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22 #  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23 #  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
24 #  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
25 #  FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
26 #  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
27 #  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
28 #  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
29 #  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30 
31 from typing import Dict
32 
33 from sanic import Sanic
34 from sanic import __version__ as version
35 from sanic.cookies import CookieJar
36 from sanic.request import Request
37 from sanic.response import HTTPResponse
38 
39 from elasticapm.base import Client
40 from elasticapm.conf import Config, constants
41 from elasticapm.contrib.sanic.sanic_types import EnvInfoType
42 from elasticapm.utils import compat, get_url_dict
43 
44 
45 class SanicAPMConfig(dict):
46     def __init__(self, app: Sanic):
47         super(SanicAPMConfig, self).__init__()
48         for _key, _v in app.config.items():
49             if _key.startswith("ELASTIC_APM_"):
50                 self[_key.replace("ELASTIC_APM_", "")] = _v
51 
52 
53 def get_env(request: Request) -> EnvInfoType:
54     """
55     Extract Server Environment Information from the current Request's context
56     :param request: Inbound HTTP Request
57     :return: A tuple containing the attribute and it's corresponding value for the current Application ENV
58     """
59     for _attr in ("server_name", "server_port", "version"):
60         if hasattr(request, _attr):
61             yield _attr, getattr(request, _attr)
62 
63 
64 # noinspection PyBroadException
65 async def get_request_info(config: Config, request: Request) -> Dict[str, str]:
66     """
67     Generate a traceable context information from the inbound HTTP request
68 
69     :param config: Application Configuration used to tune the way the data is captured
70     :param request: Inbound HTTP request
71     :return: A dictionary containing the context information of the ongoing transaction
72     """
73     env = dict(get_env(request=request))
74     env.update(dict(request.app.config))
75     result = {
76         "env": env,
77         "method": request.method,
78         "socket": {
79             "remote_address": _get_client_ip(request=request),
80             "encrypted": request.scheme in ["https", "wss"],
81         },
82         "cookies": request.cookies,
83         "http_version": request.version,
84     }
85     if config.capture_headers:
86         result["headers"] = dict(request.headers)
87 
88     if request.method in constants.HTTP_WITH_BODY and config.capture_body:
89         if request.content_type.startswith("multipart") or "octet-stream" in request.content_type:
90             result["body"] = "[DISCARDED]"
91         try:
92             result["body"] = request.body.decode("utf-8")
93         except Exception:
94             pass
95 
96     if "body" not in result:
97         result["body"] = "[REDACTED]"
98     result["url"] = get_url_dict(request.url)
99     return result
100 
101 
102 async def get_response_info(config: Config, response: HTTPResponse) -> Dict[str, str]:
103     """
104     Generate a traceable context information from the inbound HTTP Response
105 
106     :param config: Application Configuration used to tune the way the data is captured
107     :param response: outbound HTTP Response
108     :return: A dictionary containing the context information of the ongoing transaction
109     """
110     result = {
111         "cookies": _transform_response_cookie(cookies=response.cookies),
112         "finished": True,
113         "headers_sent": True,
114     }
115     if isinstance(response.status, compat.integer_types):
116         result["status_code"] = response.status
117 
118     if config.capture_headers:
119         result["headers"] = dict(response.headers)
120 
121     if config.capture_body and "octet-stream" not in response.content_type:
122         result["body"] = response.body.decode("utf-8")
123     else:
124         result["body"] = "[REDACTED]"
125 
126     return result
127 
128 
129 def _get_client_ip(request: Request) -> str:
130     """Extract Client IP Address Information"""
131     try:
132         return request.ip or request.socket[0] or request.remote_addr
133     except IndexError:
134         return request.remote_addr
135 
136 
137 def make_client(client_cls=Client, **defaults) -> Client:
138     if "framework_name" not in defaults:
139         defaults["framework_name"] = "sanic"
140         defaults["framework_version"] = version
141 
142     return client_cls(**defaults)
143 
144 
145 def _transform_response_cookie(cookies: CookieJar) -> Dict[str, str]:
146     """Transform the Sanic's CookieJar instance into a Normal dictionary to build the context"""
147     return {k: {"value": v.value, "path": v["path"]} for k, v in cookies.items()}
148
```
--- 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/elasticapm/contrib/sanic/utils.py b/elasticapm/contrib/sanic/utils.py
--- a/elasticapm/contrib/sanic/utils.py
+++ b/elasticapm/contrib/sanic/utils.py
@@ -28,6 +28,7 @@
 #  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 #  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 
+from string import ascii_uppercase
 from typing import Dict
 
 from sanic import Sanic
@@ -71,7 +72,8 @@
     :return: A dictionary containing the context information of the ongoing transaction
     """
     env = dict(get_env(request=request))
-    env.update(dict(request.app.config))
+    app_config = {k: v for k, v in dict(request.app.config).items() if all(letter in ascii_uppercase for letter in k)}
+    env.update(app_config)
     result = {
         "env": env,
         "method": request.method,

{"golden_diff": "diff --git a/elasticapm/contrib/sanic/utils.py b/elasticapm/contrib/sanic/utils.py\n--- a/elasticapm/contrib/sanic/utils.py\n+++ b/elasticapm/contrib/sanic/utils.py\n@@ -28,6 +28,7 @@\n #  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\n #  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n \n+from string import ascii_uppercase\n from typing import Dict\n \n from sanic import Sanic\n@@ -71,7 +72,8 @@\n     :return: A dictionary containing the context information of the ongoing transaction\n     \"\"\"\n     env = dict(get_env(request=request))\n-    env.update(dict(request.app.config))\n+    app_config = {k: v for k, v in dict(request.app.config).items() if all(letter in ascii_uppercase for letter in k)}\n+    env.update(app_config)\n     result = {\n         \"env\": env,\n         \"method\": request.method,\n", "issue": "Sanic 21.9.2 breaks exception tracking\nFor yet unknown reasons, Sanic 21.9.2+ broke exception tracking. The changes between 21.9.1 and 21.9.2 are here:\r\n\r\nhttps://github.com/sanic-org/sanic/compare/v21.9.1...v21.9.2\r\n\r\nThe test failures are here: https://apm-ci.elastic.co/blue/organizations/jenkins/apm-agent-python%2Fapm-agent-python-nightly-mbp%2Fmaster/detail/master/787/tests/\r\n\r\nExample:\r\n\r\n    ----------------------------------------------------- Captured log call ------------------------------------------------------INFO     sanic.root:testing.py:82 http://127.0.0.1:50003/fallback-value-error\r\n    ERROR    sanic.error:request.py:193 Exception occurred in one of response middleware handlers\r\n    Traceback (most recent call last):\r\n    File \"/home/user/.local/lib/python3.10/site-packages/sanic_routing/router.py\", line 79, in resolve\r\n        route, param_basket = self.find_route(\r\n    File \"\", line 24, in find_route\r\n    sanic_routing.exceptions.NotFound: Not Found\r\n\r\n    During handling of the above exception, another exception occurred:\r\n\r\n    Traceback (most recent call last):\r\n    File \"/home/user/.local/lib/python3.10/site-packages/sanic/router.py\", line 38, in _get\r\n        return self.resolve(\r\n    File \"/home/user/.local/lib/python3.10/site-packages/sanic_routing/router.py\", line 96, in resolve\r\n        raise self.exception(str(e), path=path)\r\n    sanic_routing.exceptions.NotFound: Not Found\r\n\r\n    During handling of the above exception, another exception occurred:\r\n\r\n    Traceback (most recent call last):\r\n    File \"handle_request\", line 26, in handle_request\r\n        Any,\r\n    File \"/home/user/.local/lib/python3.10/site-packages/sanic/router.py\", line 66, in get\r\n        return self._get(path, method, host)\r\n    File \"/home/user/.local/lib/python3.10/site-packages/sanic/router.py\", line 44, in _get\r\n        raise NotFound(\"Requested URL {} not found\".format(e.path))\r\n    sanic.exceptions.NotFound: Requested URL /fallback-value-error not found\r\n\r\n    During handling of the above exception, another exception occurred:\r\n\r\n    Traceback (most recent call last):\r\n    File \"/home/user/.local/lib/python3.10/site-packages/sanic/request.py\", line 187, in respond\r\n        response = await self.app._run_response_middleware(\r\n    File \"_run_response_middleware\", line 22, in _run_response_middleware\r\n        from ssl import Purpose, SSLContext, create_default_context\r\n    File \"/app/elasticapm/contrib/sanic/__init__.py\", line 279, in _instrument_response\r\n        await set_context(\r\n    File \"/app/elasticapm/contrib/asyncio/traces.py\", line 93, in set_context\r\n        data = await data()\r\n    File \"/app/elasticapm/contrib/sanic/utils.py\", line 121, in get_response_info\r\n        if config.capture_body and \"octet-stream\" not in response.content_type:\r\n    TypeError: argument of type 'NoneType' is not iterable\r\n\r\nChecking for `response.content_type is not None` in `elasticapm/contrib/sanic/utils.py:121` doesn't resolve the issue.\r\n\r\n@ahopkins do you happen to have an idea what could cause these failures?\n", "before_files": [{"content": "#  BSD 3-Clause License\n#\n#  Copyright (c) 2012, the Sentry Team, see AUTHORS for more details\n#  Copyright (c) 2019, Elasticsearch BV\n#  All rights reserved.\n#\n#  Redistribution and use in source and binary forms, with or without\n#  modification, are permitted provided that the following conditions are met:\n#\n#  * Redistributions of source code must retain the above copyright notice, this\n#    list of conditions and the following disclaimer.\n#\n#  * Redistributions in binary form must reproduce the above copyright notice,\n#    this list of conditions and the following disclaimer in the documentation\n#    and/or other materials provided with the distribution.\n#\n#  * Neither the name of the copyright holder nor the names of its\n#    contributors may be used to endorse or promote products derived from\n#    this software without specific prior written permission.\n#\n#  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n#  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n#  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n#  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\n#  FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\n#  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\n#  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\n#  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\n#  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n\nfrom typing import Dict\n\nfrom sanic import Sanic\nfrom sanic import __version__ as version\nfrom sanic.cookies import CookieJar\nfrom sanic.request import Request\nfrom sanic.response import HTTPResponse\n\nfrom elasticapm.base import Client\nfrom elasticapm.conf import Config, constants\nfrom elasticapm.contrib.sanic.sanic_types import EnvInfoType\nfrom elasticapm.utils import compat, get_url_dict\n\n\nclass SanicAPMConfig(dict):\n    def __init__(self, app: Sanic):\n        super(SanicAPMConfig, self).__init__()\n        for _key, _v in app.config.items():\n            if _key.startswith(\"ELASTIC_APM_\"):\n                self[_key.replace(\"ELASTIC_APM_\", \"\")] = _v\n\n\ndef get_env(request: Request) -> EnvInfoType:\n    \"\"\"\n    Extract Server Environment Information from the current Request's context\n    :param request: Inbound HTTP Request\n    :return: A tuple containing the attribute and it's corresponding value for the current Application ENV\n    \"\"\"\n    for _attr in (\"server_name\", \"server_port\", \"version\"):\n        if hasattr(request, _attr):\n            yield _attr, getattr(request, _attr)\n\n\n# noinspection PyBroadException\nasync def get_request_info(config: Config, request: Request) -> Dict[str, str]:\n    \"\"\"\n    Generate a traceable context information from the inbound HTTP request\n\n    :param config: Application Configuration used to tune the way the data is captured\n    :param request: Inbound HTTP request\n    :return: A dictionary containing the context information of the ongoing transaction\n    \"\"\"\n    env = dict(get_env(request=request))\n    env.update(dict(request.app.config))\n    result = {\n        \"env\": env,\n        \"method\": request.method,\n        \"socket\": {\n            \"remote_address\": _get_client_ip(request=request),\n            \"encrypted\": request.scheme in [\"https\", \"wss\"],\n        },\n        \"cookies\": request.cookies,\n        \"http_version\": request.version,\n    }\n    if config.capture_headers:\n        result[\"headers\"] = dict(request.headers)\n\n    if request.method in constants.HTTP_WITH_BODY and config.capture_body:\n        if request.content_type.startswith(\"multipart\") or \"octet-stream\" in request.content_type:\n            result[\"body\"] = \"[DISCARDED]\"\n        try:\n            result[\"body\"] = request.body.decode(\"utf-8\")\n        except Exception:\n            pass\n\n    if \"body\" not in result:\n        result[\"body\"] = \"[REDACTED]\"\n    result[\"url\"] = get_url_dict(request.url)\n    return result\n\n\nasync def get_response_info(config: Config, response: HTTPResponse) -> Dict[str, str]:\n    \"\"\"\n    Generate a traceable context information from the inbound HTTP Response\n\n    :param config: Application Configuration used to tune the way the data is captured\n    :param response: outbound HTTP Response\n    :return: A dictionary containing the context information of the ongoing transaction\n    \"\"\"\n    result = {\n        \"cookies\": _transform_response_cookie(cookies=response.cookies),\n        \"finished\": True,\n        \"headers_sent\": True,\n    }\n    if isinstance(response.status, compat.integer_types):\n        result[\"status_code\"] = response.status\n\n    if config.capture_headers:\n        result[\"headers\"] = dict(response.headers)\n\n    if config.capture_body and \"octet-stream\" not in response.content_type:\n        result[\"body\"] = response.body.decode(\"utf-8\")\n    else:\n        result[\"body\"] = \"[REDACTED]\"\n\n    return result\n\n\ndef _get_client_ip(request: Request) -> str:\n    \"\"\"Extract Client IP Address Information\"\"\"\n    try:\n        return request.ip or request.socket[0] or request.remote_addr\n    except IndexError:\n        return request.remote_addr\n\n\ndef make_client(client_cls=Client, **defaults) -> Client:\n    if \"framework_name\" not in defaults:\n        defaults[\"framework_name\"] = \"sanic\"\n        defaults[\"framework_version\"] = version\n\n    return client_cls(**defaults)\n\n\ndef _transform_response_cookie(cookies: CookieJar) -> Dict[str, str]:\n    \"\"\"Transform the Sanic's CookieJar instance into a Normal dictionary to build the context\"\"\"\n    return {k: {\"value\": v.value, \"path\": v[\"path\"]} for k, v in cookies.items()}\n", "path": "elasticapm/contrib/sanic/utils.py"}], "after_files": [{"content": "#  BSD 3-Clause License\n#\n#  Copyright (c) 2012, the Sentry Team, see AUTHORS for more details\n#  Copyright (c) 2019, Elasticsearch BV\n#  All rights reserved.\n#\n#  Redistribution and use in source and binary forms, with or without\n#  modification, are permitted provided that the following conditions are met:\n#\n#  * Redistributions of source code must retain the above copyright notice, this\n#    list of conditions and the following disclaimer.\n#\n#  * Redistributions in binary form must reproduce the above copyright notice,\n#    this list of conditions and the following disclaimer in the documentation\n#    and/or other materials provided with the distribution.\n#\n#  * Neither the name of the copyright holder nor the names of its\n#    contributors may be used to endorse or promote products derived from\n#    this software without specific prior written permission.\n#\n#  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n#  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n#  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n#  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\n#  FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\n#  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\n#  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\n#  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\n#  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n\nfrom string import ascii_uppercase\nfrom typing import Dict\n\nfrom sanic import Sanic\nfrom sanic import __version__ as version\nfrom sanic.cookies import CookieJar\nfrom sanic.request import Request\nfrom sanic.response import HTTPResponse\n\nfrom elasticapm.base import Client\nfrom elasticapm.conf import Config, constants\nfrom elasticapm.contrib.sanic.sanic_types import EnvInfoType\nfrom elasticapm.utils import compat, get_url_dict\n\n\nclass SanicAPMConfig(dict):\n    def __init__(self, app: Sanic):\n        super(SanicAPMConfig, self).__init__()\n        for _key, _v in app.config.items():\n            if _key.startswith(\"ELASTIC_APM_\"):\n                self[_key.replace(\"ELASTIC_APM_\", \"\")] = _v\n\n\ndef get_env(request: Request) -> EnvInfoType:\n    \"\"\"\n    Extract Server Environment Information from the current Request's context\n    :param request: Inbound HTTP Request\n    :return: A tuple containing the attribute and it's corresponding value for the current Application ENV\n    \"\"\"\n    for _attr in (\"server_name\", \"server_port\", \"version\"):\n        if hasattr(request, _attr):\n            yield _attr, getattr(request, _attr)\n\n\n# noinspection PyBroadException\nasync def get_request_info(config: Config, request: Request) -> Dict[str, str]:\n    \"\"\"\n    Generate a traceable context information from the inbound HTTP request\n\n    :param config: Application Configuration used to tune the way the data is captured\n    :param request: Inbound HTTP request\n    :return: A dictionary containing the context information of the ongoing transaction\n    \"\"\"\n    env = dict(get_env(request=request))\n    app_config = {k: v for k, v in dict(request.app.config).items() if all(letter in ascii_uppercase for letter in k)}\n    env.update(app_config)\n    result = {\n        \"env\": env,\n        \"method\": request.method,\n        \"socket\": {\n            \"remote_address\": _get_client_ip(request=request),\n            \"encrypted\": request.scheme in [\"https\", \"wss\"],\n        },\n        \"cookies\": request.cookies,\n        \"http_version\": request.version,\n    }\n    if config.capture_headers:\n        result[\"headers\"] = dict(request.headers)\n\n    if request.method in constants.HTTP_WITH_BODY and config.capture_body:\n        if request.content_type.startswith(\"multipart\") or \"octet-stream\" in request.content_type:\n            result[\"body\"] = \"[DISCARDED]\"\n        try:\n            result[\"body\"] = request.body.decode(\"utf-8\")\n        except Exception:\n            pass\n\n    if \"body\" not in result:\n        result[\"body\"] = \"[REDACTED]\"\n    result[\"url\"] = get_url_dict(request.url)\n    return result\n\n\nasync def get_response_info(config: Config, response: HTTPResponse) -> Dict[str, str]:\n    \"\"\"\n    Generate a traceable context information from the inbound HTTP Response\n\n    :param config: Application Configuration used to tune the way the data is captured\n    :param response: outbound HTTP Response\n    :return: A dictionary containing the context information of the ongoing transaction\n    \"\"\"\n    result = {\n        \"cookies\": _transform_response_cookie(cookies=response.cookies),\n        \"finished\": True,\n        \"headers_sent\": True,\n    }\n    if isinstance(response.status, compat.integer_types):\n        result[\"status_code\"] = response.status\n\n    if config.capture_headers:\n        result[\"headers\"] = dict(response.headers)\n\n    if config.capture_body and \"octet-stream\" not in response.content_type:\n        result[\"body\"] = response.body.decode(\"utf-8\")\n    else:\n        result[\"body\"] = \"[REDACTED]\"\n\n    return result\n\n\ndef _get_client_ip(request: Request) -> str:\n    \"\"\"Extract Client IP Address Information\"\"\"\n    try:\n        return request.ip or request.socket[0] or request.remote_addr\n    except IndexError:\n        return request.remote_addr\n\n\ndef make_client(client_cls=Client, **defaults) -> Client:\n    if \"framework_name\" not in defaults:\n        defaults[\"framework_name\"] = \"sanic\"\n        defaults[\"framework_version\"] = version\n\n    return client_cls(**defaults)\n\n\ndef _transform_response_cookie(cookies: CookieJar) -> Dict[str, str]:\n    \"\"\"Transform the Sanic's CookieJar instance into a Normal dictionary to build the context\"\"\"\n    return {k: {\"value\": v.value, \"path\": v[\"path\"]} for k, v in cookies.items()}\n", "path": "elasticapm/contrib/sanic/utils.py"}]}