Repository: elastic/apm-agent-python

Issue:
Stop using undocumented get_raw_uri in DjangoClient
Django 4.0 removes the `request.get_raw_uri` method (without a deprecation period) because it is undocumented/private and considered unsafe (https://code.djangoproject.com/ticket/32698#comment:11).

This method is used here (guarded by a `hasattr(request, "get_raw_uri")` check):

https://github.com/elastic/apm-agent-python/blob/484ba5a1ad07e59bd4adeaf0291cc3da23d76afa/elasticapm/contrib/django/client.py#L155-L167

According to the ticket description (https://code.djangoproject.com/ticket/32698) "Niche use cases, such as APM packages [...], can read the internal attributes of HttpRequest instead.". I assume this means that e.g. the elastic-apm client can recreate the same behaviour by replicating the now removed `get_raw_uri` method.

Looking at the commit that removes this method (https://github.com/django/django/commit/8bcb00858e0ddec79cc96669c238d29c30d7effb) this would work:

```
url = '{scheme}://{host}{path}'.format(
    scheme=request.scheme,
    host=request._get_raw_host(),
    path=request.get_full_path(),
)
```

This means using a private/undocumented method (`request._get_raw_host`) but I see no other alternative.


Discussion:
@jaap3 thanks for the heads up! I opened #1343 to add Django 4.0 to our test matrix, and a test related to this does indeed fail. I agree that copying the now-removed implementation of `get_raw_uri()` seems like the most sensible way forward.

BTW, contrary to what is said in that Django ticket, we didn't stumble over `get_raw_uri()`, we've been [pointed towards it](https://code.djangoproject.com/ticket/27506#comment:3) by a Django core dev :grinning: 
I might be worthwhile pointing this out to the Django devs. I tried but they weren't very receptive. It's odd that the only way forward is again relying on a private API (at least this time it's clear that the API is private :)).

Resolving patch:
diff --git a/elasticapm/contrib/django/client.py b/elasticapm/contrib/django/client.py
--- a/elasticapm/contrib/django/client.py
+++ b/elasticapm/contrib/django/client.py
@@ -33,7 +33,6 @@
 
 import django
 from django.conf import settings as django_settings
-from django.core.exceptions import DisallowedHost
 from django.db import DatabaseError
 from django.http import HttpRequest
 
@@ -45,7 +44,7 @@
 from elasticapm import get_client as _get_client
 from elasticapm.base import Client
 from elasticapm.conf import constants
-from elasticapm.contrib.django.utils import iterate_with_template_sources
+from elasticapm.contrib.django.utils import get_raw_uri, iterate_with_template_sources
 from elasticapm.utils import compat, encoding, get_url_dict
 from elasticapm.utils.logging import get_logger
 from elasticapm.utils.module_import import import_string
@@ -152,21 +151,8 @@ def get_data_from_request(self, request, event_type):
                 if data is not None:
                     result["body"] = data
 
-        if hasattr(request, "get_raw_uri"):
-            # added in Django 1.9
-            url = request.get_raw_uri()
-        else:
-            try:
-                # Requires host to be in ALLOWED_HOSTS, might throw a
-                # DisallowedHost exception
-                url = request.build_absolute_uri()
-            except DisallowedHost:
-                # We can't figure out the real URL, so we have to set it to
-                # DisallowedHost
-                result["url"] = {"full": "DisallowedHost"}
-                url = None
-        if url:
-            result["url"] = get_url_dict(url)
+        url = get_raw_uri(request)
+        result["url"] = get_url_dict(url)
         return result
 
     def get_data_from_response(self, response, event_type):
diff --git a/elasticapm/contrib/django/utils.py b/elasticapm/contrib/django/utils.py
--- a/elasticapm/contrib/django/utils.py
+++ b/elasticapm/contrib/django/utils.py
@@ -31,6 +31,7 @@
 
 import logging
 
+from django.http import HttpRequest
 from django.template.base import Node
 
 from elasticapm.utils.stacks import get_frame_info
@@ -91,3 +92,20 @@ def iterate_with_template_sources(
             exclude_paths_re=exclude_paths_re,
             locals_processor_func=locals_processor_func,
         )
+
+
+def get_raw_uri(request: HttpRequest) -> str:
+    """
+    Return an absolute URI from variables available in this request. Skip
+    allowed hosts protection, so may return insecure URI.
+
+    Re-implementation of Request.get_raw_uri that was removed with Django 4.0.
+
+    :param request: a Request object
+    :return: the URL
+    """
+    return "{scheme}://{host}{path}".format(
+        scheme=request.scheme,
+        host=request._get_raw_host(),
+        path=request.get_full_path(),
+    )


Test patch: