We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Missing error message: Oops. That didn't work. The following error occured:
**Describe the bug**: No error message appear

```
>> python3 manage.py elasticapm test

Trying to send a test error to APM Server using these settings:

SERVICE_NAME:	DJANGO_DEVELOPMENT
SECRET_TOKEN:	xxx
SERVER:		http://localhost:8200

Oops. That didn't work. The following error occured: 

Fetched APM Server version 7.17.0
```

**To Reproduce**

Not sure how to reproduce this. 
Where can I find a detailed error message?


--- 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/transport/http.py`
Content:
```
1 # -*- coding: utf-8 -*-
2 
3 #  BSD 3-Clause License
4 #
5 #  Copyright (c) 2019, Elasticsearch BV
6 #  All rights reserved.
7 #
8 #  Redistribution and use in source and binary forms, with or without
9 #  modification, are permitted provided that the following conditions are met:
10 #
11 #  * Redistributions of source code must retain the above copyright notice, this
12 #    list of conditions and the following disclaimer.
13 #
14 #  * Redistributions in binary form must reproduce the above copyright notice,
15 #    this list of conditions and the following disclaimer in the documentation
16 #    and/or other materials provided with the distribution.
17 #
18 #  * Neither the name of the copyright holder nor the names of its
19 #    contributors may be used to endorse or promote products derived from
20 #    this software without specific prior written permission.
21 #
22 #  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
23 #  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24 #  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
25 #  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
26 #  FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
27 #  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
28 #  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
29 #  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
30 #  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
31 #  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32 
33 import hashlib
34 import json
35 import re
36 import ssl
37 import urllib.parse
38 from urllib.request import getproxies_environment, proxy_bypass_environment
39 
40 import urllib3
41 from urllib3.exceptions import MaxRetryError, TimeoutError
42 
43 from elasticapm.transport.exceptions import TransportException
44 from elasticapm.transport.http_base import HTTPTransportBase
45 from elasticapm.utils import json_encoder, read_pem_file
46 from elasticapm.utils.logging import get_logger
47 
48 try:
49     import certifi
50 except ImportError:
51     certifi = None
52 
53 logger = get_logger("elasticapm.transport.http")
54 
55 
56 class Transport(HTTPTransportBase):
57     def __init__(self, url: str, *args, **kwargs) -> None:
58         super(Transport, self).__init__(url, *args, **kwargs)
59         pool_kwargs = {"cert_reqs": "CERT_REQUIRED", "ca_certs": self.ca_certs, "block": True}
60         if url.startswith("https"):
61             if self._server_cert:
62                 pool_kwargs.update(
63                     {"assert_fingerprint": self.cert_fingerprint, "assert_hostname": False, "cert_reqs": ssl.CERT_NONE}
64                 )
65                 del pool_kwargs["ca_certs"]
66             elif not self._verify_server_cert:
67                 pool_kwargs["cert_reqs"] = ssl.CERT_NONE
68                 pool_kwargs["assert_hostname"] = False
69         self._pool_kwargs = pool_kwargs
70         self._http = None
71         self._url = url
72 
73     def send(self, data):
74         response = None
75 
76         headers = self._headers.copy() if self._headers else {}
77         headers.update(self.auth_headers)
78 
79         url = self._url
80         try:
81             try:
82                 response = self.http.urlopen(
83                     "POST", url, body=data, headers=headers, timeout=self._timeout, preload_content=False
84                 )
85                 logger.debug("Sent request, url=%s size=%.2fkb status=%s", url, len(data) / 1024.0, response.status)
86             except Exception as e:
87                 print_trace = True
88                 if isinstance(e, MaxRetryError) and isinstance(e.reason, TimeoutError):
89                     message = "Connection to APM Server timed out " "(url: %s, timeout: %s seconds)" % (
90                         self._url,
91                         self._timeout,
92                     )
93                     print_trace = False
94                 else:
95                     message = "Unable to reach APM Server: %s (url: %s)" % (e, self._url)
96                 raise TransportException(message, data, print_trace=print_trace)
97             body = response.read()
98             if response.status >= 400:
99                 if response.status == 429:  # rate-limited
100                     message = "Temporarily rate limited: "
101                     print_trace = False
102                 else:
103                     message = "HTTP %s: " % response.status
104                     print_trace = True
105                 message += body.decode("utf8", errors="replace")[:10000]
106                 raise TransportException(message, data, print_trace=print_trace)
107             return response.getheader("Location")
108         finally:
109             if response:
110                 response.close()
111 
112     @property
113     def http(self) -> urllib3.PoolManager:
114         if not self._http:
115             url_parts = urllib.parse.urlparse(self._url)
116             proxies = getproxies_environment()
117             proxy_url = proxies.get("https", proxies.get("http", None))
118             if proxy_url and not proxy_bypass_environment(url_parts.netloc):
119                 self._http = urllib3.ProxyManager(proxy_url, **self._pool_kwargs)
120             else:
121                 self._http = urllib3.PoolManager(**self._pool_kwargs)
122         return self._http
123 
124     def handle_fork(self) -> None:
125         # reset http pool to avoid sharing connections with the parent process
126         self._http = None
127 
128     def get_config(self, current_version=None, keys=None):
129         """
130         Gets configuration from a remote APM Server
131 
132         :param current_version: version of the current configuration
133         :param keys: a JSON-serializable dict to identify this instance, e.g.
134                 {
135                     "service": {
136                         "name": "foo",
137                         "environment": "bar"
138                     }
139                 }
140         :return: a three-tuple of new version, config dictionary and validity in seconds.
141                  Any element of the tuple can be None.
142         """
143         url = self._config_url
144         data = json_encoder.dumps(keys).encode("utf-8")
145         headers = self._headers.copy()
146         headers[b"Content-Type"] = "application/json"
147         headers.pop(b"Content-Encoding", None)  # remove gzip content-encoding header
148         headers.update(self.auth_headers)
149         max_age = 300
150         if current_version:
151             headers["If-None-Match"] = current_version
152         try:
153             response = self.http.urlopen(
154                 "POST", url, body=data, headers=headers, timeout=self._timeout, preload_content=False
155             )
156         except (urllib3.exceptions.RequestError, urllib3.exceptions.HTTPError) as e:
157             logger.debug("HTTP error while fetching remote config: %s", str(e))
158             return current_version, None, max_age
159         body = response.read()
160         if "Cache-Control" in response.headers:
161             try:
162                 max_age = int(next(re.finditer(r"max-age=(\d+)", response.headers["Cache-Control"])).groups()[0])
163             except StopIteration:
164                 logger.debug("Could not parse Cache-Control header: %s", response.headers["Cache-Control"])
165         if response.status == 304:
166             # config is unchanged, return
167             logger.debug("Configuration unchanged")
168             return current_version, None, max_age
169         elif response.status >= 400:
170             return None, None, max_age
171 
172         if not body:
173             logger.debug("APM Server answered with empty body and status code %s", response.status)
174             return current_version, None, max_age
175         body = body.decode("utf-8")
176         try:
177             data = json_encoder.loads(body)
178             return response.headers.get("Etag"), data, max_age
179         except json.JSONDecodeError:
180             logger.warning("Failed decoding APM Server response as JSON: %s", body)
181             return current_version, None, max_age
182 
183     def _process_queue(self):
184         if not self.client.server_version:
185             self.fetch_server_info()
186         super()._process_queue()
187 
188     def fetch_server_info(self):
189         headers = self._headers.copy() if self._headers else {}
190         headers.update(self.auth_headers)
191         headers["accept"] = "text/plain"
192         try:
193             response = self.http.urlopen("GET", self._server_info_url, headers=headers, timeout=self._timeout)
194             body = response.data
195             data = json_encoder.loads(body.decode("utf8"))
196             version = data["version"]
197             logger.info("Fetched APM Server version %s", version)
198             self.client.server_version = version_string_to_tuple(version)
199         except (urllib3.exceptions.RequestError, urllib3.exceptions.HTTPError) as e:
200             logger.warning("HTTP error while fetching server information: %s", str(e))
201         except json.JSONDecodeError as e:
202             logger.warning("JSON decoding error while fetching server information: %s", str(e))
203         except (KeyError, TypeError):
204             logger.warning("No version key found in server response: %s", response.data)
205 
206     @property
207     def cert_fingerprint(self):
208         if self._server_cert:
209             with open(self._server_cert, "rb") as f:
210                 cert_data = read_pem_file(f)
211             digest = hashlib.sha256()
212             digest.update(cert_data)
213             return digest.hexdigest()
214         return None
215 
216     @property
217     def auth_headers(self):
218         headers = super(Transport, self).auth_headers
219         return {k.encode("ascii"): v.encode("ascii") for k, v in headers.items()}
220 
221     @property
222     def ca_certs(self):
223         """
224         Return location of certificate store. If it is available and not disabled via setting,
225         this will return the location of the certifi certificate store.
226         """
227         return certifi.where() if (certifi and self.client.config.use_certifi) else None
228 
229 
230 def version_string_to_tuple(version):
231     if version:
232         version_parts = re.split(r"[.\-]", version)
233         return tuple(int(p) if p.isdigit() else p for p in version_parts)
234     return ()
235 
236 
237 # left for backwards compatibility
238 AsyncTransport = Transport
239
```
--- 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/transport/http.py b/elasticapm/transport/http.py
--- a/elasticapm/transport/http.py
+++ b/elasticapm/transport/http.py
@@ -194,7 +194,7 @@
             body = response.data
             data = json_encoder.loads(body.decode("utf8"))
             version = data["version"]
-            logger.info("Fetched APM Server version %s", version)
+            logger.debug("Fetched APM Server version %s", version)
             self.client.server_version = version_string_to_tuple(version)
         except (urllib3.exceptions.RequestError, urllib3.exceptions.HTTPError) as e:
             logger.warning("HTTP error while fetching server information: %s", str(e))

{"golden_diff": "diff --git a/elasticapm/transport/http.py b/elasticapm/transport/http.py\n--- a/elasticapm/transport/http.py\n+++ b/elasticapm/transport/http.py\n@@ -194,7 +194,7 @@\n             body = response.data\n             data = json_encoder.loads(body.decode(\"utf8\"))\n             version = data[\"version\"]\n-            logger.info(\"Fetched APM Server version %s\", version)\n+            logger.debug(\"Fetched APM Server version %s\", version)\n             self.client.server_version = version_string_to_tuple(version)\n         except (urllib3.exceptions.RequestError, urllib3.exceptions.HTTPError) as e:\n             logger.warning(\"HTTP error while fetching server information: %s\", str(e))\n", "issue": "Missing error message: Oops. That didn't work. The following error occured:\n**Describe the bug**: No error message appear\r\n\r\n```\r\n>> python3 manage.py elasticapm test\r\n\r\nTrying to send a test error to APM Server using these settings:\r\n\r\nSERVICE_NAME:\tDJANGO_DEVELOPMENT\r\nSECRET_TOKEN:\txxx\r\nSERVER:\t\thttp://localhost:8200\r\n\r\nOops. That didn't work. The following error occured: \r\n\r\nFetched APM Server version 7.17.0\r\n```\r\n\r\n**To Reproduce**\r\n\r\nNot sure how to reproduce this. \r\nWhere can I find a detailed error message?\r\n\n", "before_files": [{"content": "# -*- coding: utf-8 -*-\n\n#  BSD 3-Clause License\n#\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#  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\nimport hashlib\nimport json\nimport re\nimport ssl\nimport urllib.parse\nfrom urllib.request import getproxies_environment, proxy_bypass_environment\n\nimport urllib3\nfrom urllib3.exceptions import MaxRetryError, TimeoutError\n\nfrom elasticapm.transport.exceptions import TransportException\nfrom elasticapm.transport.http_base import HTTPTransportBase\nfrom elasticapm.utils import json_encoder, read_pem_file\nfrom elasticapm.utils.logging import get_logger\n\ntry:\n    import certifi\nexcept ImportError:\n    certifi = None\n\nlogger = get_logger(\"elasticapm.transport.http\")\n\n\nclass Transport(HTTPTransportBase):\n    def __init__(self, url: str, *args, **kwargs) -> None:\n        super(Transport, self).__init__(url, *args, **kwargs)\n        pool_kwargs = {\"cert_reqs\": \"CERT_REQUIRED\", \"ca_certs\": self.ca_certs, \"block\": True}\n        if url.startswith(\"https\"):\n            if self._server_cert:\n                pool_kwargs.update(\n                    {\"assert_fingerprint\": self.cert_fingerprint, \"assert_hostname\": False, \"cert_reqs\": ssl.CERT_NONE}\n                )\n                del pool_kwargs[\"ca_certs\"]\n            elif not self._verify_server_cert:\n                pool_kwargs[\"cert_reqs\"] = ssl.CERT_NONE\n                pool_kwargs[\"assert_hostname\"] = False\n        self._pool_kwargs = pool_kwargs\n        self._http = None\n        self._url = url\n\n    def send(self, data):\n        response = None\n\n        headers = self._headers.copy() if self._headers else {}\n        headers.update(self.auth_headers)\n\n        url = self._url\n        try:\n            try:\n                response = self.http.urlopen(\n                    \"POST\", url, body=data, headers=headers, timeout=self._timeout, preload_content=False\n                )\n                logger.debug(\"Sent request, url=%s size=%.2fkb status=%s\", url, len(data) / 1024.0, response.status)\n            except Exception as e:\n                print_trace = True\n                if isinstance(e, MaxRetryError) and isinstance(e.reason, TimeoutError):\n                    message = \"Connection to APM Server timed out \" \"(url: %s, timeout: %s seconds)\" % (\n                        self._url,\n                        self._timeout,\n                    )\n                    print_trace = False\n                else:\n                    message = \"Unable to reach APM Server: %s (url: %s)\" % (e, self._url)\n                raise TransportException(message, data, print_trace=print_trace)\n            body = response.read()\n            if response.status >= 400:\n                if response.status == 429:  # rate-limited\n                    message = \"Temporarily rate limited: \"\n                    print_trace = False\n                else:\n                    message = \"HTTP %s: \" % response.status\n                    print_trace = True\n                message += body.decode(\"utf8\", errors=\"replace\")[:10000]\n                raise TransportException(message, data, print_trace=print_trace)\n            return response.getheader(\"Location\")\n        finally:\n            if response:\n                response.close()\n\n    @property\n    def http(self) -> urllib3.PoolManager:\n        if not self._http:\n            url_parts = urllib.parse.urlparse(self._url)\n            proxies = getproxies_environment()\n            proxy_url = proxies.get(\"https\", proxies.get(\"http\", None))\n            if proxy_url and not proxy_bypass_environment(url_parts.netloc):\n                self._http = urllib3.ProxyManager(proxy_url, **self._pool_kwargs)\n            else:\n                self._http = urllib3.PoolManager(**self._pool_kwargs)\n        return self._http\n\n    def handle_fork(self) -> None:\n        # reset http pool to avoid sharing connections with the parent process\n        self._http = None\n\n    def get_config(self, current_version=None, keys=None):\n        \"\"\"\n        Gets configuration from a remote APM Server\n\n        :param current_version: version of the current configuration\n        :param keys: a JSON-serializable dict to identify this instance, e.g.\n                {\n                    \"service\": {\n                        \"name\": \"foo\",\n                        \"environment\": \"bar\"\n                    }\n                }\n        :return: a three-tuple of new version, config dictionary and validity in seconds.\n                 Any element of the tuple can be None.\n        \"\"\"\n        url = self._config_url\n        data = json_encoder.dumps(keys).encode(\"utf-8\")\n        headers = self._headers.copy()\n        headers[b\"Content-Type\"] = \"application/json\"\n        headers.pop(b\"Content-Encoding\", None)  # remove gzip content-encoding header\n        headers.update(self.auth_headers)\n        max_age = 300\n        if current_version:\n            headers[\"If-None-Match\"] = current_version\n        try:\n            response = self.http.urlopen(\n                \"POST\", url, body=data, headers=headers, timeout=self._timeout, preload_content=False\n            )\n        except (urllib3.exceptions.RequestError, urllib3.exceptions.HTTPError) as e:\n            logger.debug(\"HTTP error while fetching remote config: %s\", str(e))\n            return current_version, None, max_age\n        body = response.read()\n        if \"Cache-Control\" in response.headers:\n            try:\n                max_age = int(next(re.finditer(r\"max-age=(\\d+)\", response.headers[\"Cache-Control\"])).groups()[0])\n            except StopIteration:\n                logger.debug(\"Could not parse Cache-Control header: %s\", response.headers[\"Cache-Control\"])\n        if response.status == 304:\n            # config is unchanged, return\n            logger.debug(\"Configuration unchanged\")\n            return current_version, None, max_age\n        elif response.status >= 400:\n            return None, None, max_age\n\n        if not body:\n            logger.debug(\"APM Server answered with empty body and status code %s\", response.status)\n            return current_version, None, max_age\n        body = body.decode(\"utf-8\")\n        try:\n            data = json_encoder.loads(body)\n            return response.headers.get(\"Etag\"), data, max_age\n        except json.JSONDecodeError:\n            logger.warning(\"Failed decoding APM Server response as JSON: %s\", body)\n            return current_version, None, max_age\n\n    def _process_queue(self):\n        if not self.client.server_version:\n            self.fetch_server_info()\n        super()._process_queue()\n\n    def fetch_server_info(self):\n        headers = self._headers.copy() if self._headers else {}\n        headers.update(self.auth_headers)\n        headers[\"accept\"] = \"text/plain\"\n        try:\n            response = self.http.urlopen(\"GET\", self._server_info_url, headers=headers, timeout=self._timeout)\n            body = response.data\n            data = json_encoder.loads(body.decode(\"utf8\"))\n            version = data[\"version\"]\n            logger.info(\"Fetched APM Server version %s\", version)\n            self.client.server_version = version_string_to_tuple(version)\n        except (urllib3.exceptions.RequestError, urllib3.exceptions.HTTPError) as e:\n            logger.warning(\"HTTP error while fetching server information: %s\", str(e))\n        except json.JSONDecodeError as e:\n            logger.warning(\"JSON decoding error while fetching server information: %s\", str(e))\n        except (KeyError, TypeError):\n            logger.warning(\"No version key found in server response: %s\", response.data)\n\n    @property\n    def cert_fingerprint(self):\n        if self._server_cert:\n            with open(self._server_cert, \"rb\") as f:\n                cert_data = read_pem_file(f)\n            digest = hashlib.sha256()\n            digest.update(cert_data)\n            return digest.hexdigest()\n        return None\n\n    @property\n    def auth_headers(self):\n        headers = super(Transport, self).auth_headers\n        return {k.encode(\"ascii\"): v.encode(\"ascii\") for k, v in headers.items()}\n\n    @property\n    def ca_certs(self):\n        \"\"\"\n        Return location of certificate store. If it is available and not disabled via setting,\n        this will return the location of the certifi certificate store.\n        \"\"\"\n        return certifi.where() if (certifi and self.client.config.use_certifi) else None\n\n\ndef version_string_to_tuple(version):\n    if version:\n        version_parts = re.split(r\"[.\\-]\", version)\n        return tuple(int(p) if p.isdigit() else p for p in version_parts)\n    return ()\n\n\n# left for backwards compatibility\nAsyncTransport = Transport\n", "path": "elasticapm/transport/http.py"}], "after_files": [{"content": "# -*- coding: utf-8 -*-\n\n#  BSD 3-Clause License\n#\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#  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\nimport hashlib\nimport json\nimport re\nimport ssl\nimport urllib.parse\nfrom urllib.request import getproxies_environment, proxy_bypass_environment\n\nimport urllib3\nfrom urllib3.exceptions import MaxRetryError, TimeoutError\n\nfrom elasticapm.transport.exceptions import TransportException\nfrom elasticapm.transport.http_base import HTTPTransportBase\nfrom elasticapm.utils import json_encoder, read_pem_file\nfrom elasticapm.utils.logging import get_logger\n\ntry:\n    import certifi\nexcept ImportError:\n    certifi = None\n\nlogger = get_logger(\"elasticapm.transport.http\")\n\n\nclass Transport(HTTPTransportBase):\n    def __init__(self, url: str, *args, **kwargs) -> None:\n        super(Transport, self).__init__(url, *args, **kwargs)\n        pool_kwargs = {\"cert_reqs\": \"CERT_REQUIRED\", \"ca_certs\": self.ca_certs, \"block\": True}\n        if url.startswith(\"https\"):\n            if self._server_cert:\n                pool_kwargs.update(\n                    {\"assert_fingerprint\": self.cert_fingerprint, \"assert_hostname\": False, \"cert_reqs\": ssl.CERT_NONE}\n                )\n                del pool_kwargs[\"ca_certs\"]\n            elif not self._verify_server_cert:\n                pool_kwargs[\"cert_reqs\"] = ssl.CERT_NONE\n                pool_kwargs[\"assert_hostname\"] = False\n        self._pool_kwargs = pool_kwargs\n        self._http = None\n        self._url = url\n\n    def send(self, data):\n        response = None\n\n        headers = self._headers.copy() if self._headers else {}\n        headers.update(self.auth_headers)\n\n        url = self._url\n        try:\n            try:\n                response = self.http.urlopen(\n                    \"POST\", url, body=data, headers=headers, timeout=self._timeout, preload_content=False\n                )\n                logger.debug(\"Sent request, url=%s size=%.2fkb status=%s\", url, len(data) / 1024.0, response.status)\n            except Exception as e:\n                print_trace = True\n                if isinstance(e, MaxRetryError) and isinstance(e.reason, TimeoutError):\n                    message = \"Connection to APM Server timed out \" \"(url: %s, timeout: %s seconds)\" % (\n                        self._url,\n                        self._timeout,\n                    )\n                    print_trace = False\n                else:\n                    message = \"Unable to reach APM Server: %s (url: %s)\" % (e, self._url)\n                raise TransportException(message, data, print_trace=print_trace)\n            body = response.read()\n            if response.status >= 400:\n                if response.status == 429:  # rate-limited\n                    message = \"Temporarily rate limited: \"\n                    print_trace = False\n                else:\n                    message = \"HTTP %s: \" % response.status\n                    print_trace = True\n                message += body.decode(\"utf8\", errors=\"replace\")[:10000]\n                raise TransportException(message, data, print_trace=print_trace)\n            return response.getheader(\"Location\")\n        finally:\n            if response:\n                response.close()\n\n    @property\n    def http(self) -> urllib3.PoolManager:\n        if not self._http:\n            url_parts = urllib.parse.urlparse(self._url)\n            proxies = getproxies_environment()\n            proxy_url = proxies.get(\"https\", proxies.get(\"http\", None))\n            if proxy_url and not proxy_bypass_environment(url_parts.netloc):\n                self._http = urllib3.ProxyManager(proxy_url, **self._pool_kwargs)\n            else:\n                self._http = urllib3.PoolManager(**self._pool_kwargs)\n        return self._http\n\n    def handle_fork(self) -> None:\n        # reset http pool to avoid sharing connections with the parent process\n        self._http = None\n\n    def get_config(self, current_version=None, keys=None):\n        \"\"\"\n        Gets configuration from a remote APM Server\n\n        :param current_version: version of the current configuration\n        :param keys: a JSON-serializable dict to identify this instance, e.g.\n                {\n                    \"service\": {\n                        \"name\": \"foo\",\n                        \"environment\": \"bar\"\n                    }\n                }\n        :return: a three-tuple of new version, config dictionary and validity in seconds.\n                 Any element of the tuple can be None.\n        \"\"\"\n        url = self._config_url\n        data = json_encoder.dumps(keys).encode(\"utf-8\")\n        headers = self._headers.copy()\n        headers[b\"Content-Type\"] = \"application/json\"\n        headers.pop(b\"Content-Encoding\", None)  # remove gzip content-encoding header\n        headers.update(self.auth_headers)\n        max_age = 300\n        if current_version:\n            headers[\"If-None-Match\"] = current_version\n        try:\n            response = self.http.urlopen(\n                \"POST\", url, body=data, headers=headers, timeout=self._timeout, preload_content=False\n            )\n        except (urllib3.exceptions.RequestError, urllib3.exceptions.HTTPError) as e:\n            logger.debug(\"HTTP error while fetching remote config: %s\", str(e))\n            return current_version, None, max_age\n        body = response.read()\n        if \"Cache-Control\" in response.headers:\n            try:\n                max_age = int(next(re.finditer(r\"max-age=(\\d+)\", response.headers[\"Cache-Control\"])).groups()[0])\n            except StopIteration:\n                logger.debug(\"Could not parse Cache-Control header: %s\", response.headers[\"Cache-Control\"])\n        if response.status == 304:\n            # config is unchanged, return\n            logger.debug(\"Configuration unchanged\")\n            return current_version, None, max_age\n        elif response.status >= 400:\n            return None, None, max_age\n\n        if not body:\n            logger.debug(\"APM Server answered with empty body and status code %s\", response.status)\n            return current_version, None, max_age\n        body = body.decode(\"utf-8\")\n        try:\n            data = json_encoder.loads(body)\n            return response.headers.get(\"Etag\"), data, max_age\n        except json.JSONDecodeError:\n            logger.warning(\"Failed decoding APM Server response as JSON: %s\", body)\n            return current_version, None, max_age\n\n    def _process_queue(self):\n        if not self.client.server_version:\n            self.fetch_server_info()\n        super()._process_queue()\n\n    def fetch_server_info(self):\n        headers = self._headers.copy() if self._headers else {}\n        headers.update(self.auth_headers)\n        headers[\"accept\"] = \"text/plain\"\n        try:\n            response = self.http.urlopen(\"GET\", self._server_info_url, headers=headers, timeout=self._timeout)\n            body = response.data\n            data = json_encoder.loads(body.decode(\"utf8\"))\n            version = data[\"version\"]\n            logger.debug(\"Fetched APM Server version %s\", version)\n            self.client.server_version = version_string_to_tuple(version)\n        except (urllib3.exceptions.RequestError, urllib3.exceptions.HTTPError) as e:\n            logger.warning(\"HTTP error while fetching server information: %s\", str(e))\n        except json.JSONDecodeError as e:\n            logger.warning(\"JSON decoding error while fetching server information: %s\", str(e))\n        except (KeyError, TypeError):\n            logger.warning(\"No version key found in server response: %s\", response.data)\n\n    @property\n    def cert_fingerprint(self):\n        if self._server_cert:\n            with open(self._server_cert, \"rb\") as f:\n                cert_data = read_pem_file(f)\n            digest = hashlib.sha256()\n            digest.update(cert_data)\n            return digest.hexdigest()\n        return None\n\n    @property\n    def auth_headers(self):\n        headers = super(Transport, self).auth_headers\n        return {k.encode(\"ascii\"): v.encode(\"ascii\") for k, v in headers.items()}\n\n    @property\n    def ca_certs(self):\n        \"\"\"\n        Return location of certificate store. If it is available and not disabled via setting,\n        this will return the location of the certifi certificate store.\n        \"\"\"\n        return certifi.where() if (certifi and self.client.config.use_certifi) else None\n\n\ndef version_string_to_tuple(version):\n    if version:\n        version_parts = re.split(r\"[.\\-]\", version)\n        return tuple(int(p) if p.isdigit() else p for p in version_parts)\n    return ()\n\n\n# left for backwards compatibility\nAsyncTransport = Transport\n", "path": "elasticapm/transport/http.py"}]}