Repository: scoutapp/scout_apm_python
Pull request #578 (2020-09-09 08:12:51)

Add a cast to string of query parameters
Fixes a broken application reported on Slack.


Patch:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index f20cd2d..87b6fdf 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -35,8 +35,9 @@ jobs:
       run: python3.8 setup.py bdist_wheel
     - name: Build binary wheels
       env:
-        # Disable for platforms where pure Python wheels would be generated.
-        CIBW_SKIP: "cp27-* pp27-* pp36-*"
+        # Disable for platforms where pure Python wheels would be generated,
+        # and Python 3.9 since it's pre-release
+        CIBW_SKIP: "cp27-* cp39-* pp27-* pp36-*"
       run: cibuildwheel
     - name: Check packages
       run: twine check dist/* wheelhouse/*
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9707bab..a6e9b18 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
 # Changelog
 
+## Pending
+
+### Fixed
+
+- Added a cast to string of query parameters, to work around cases where
+  application code changes them to non-strings. Fixes a possible exception:
+  `AttributeError: 'int' object has no attribute 'encode'` in
+  `scout.core.web_requests`.
+
 ## [2.16.0] 2020-09-08
 
 ### Added
diff --git a/src/scout_apm/core/web_requests.py b/src/scout_apm/core/web_requests.py
index 3116bda..f4173dd 100644
--- a/src/scout_apm/core/web_requests.py
+++ b/src/scout_apm/core/web_requests.py
@@ -4,7 +4,7 @@ from __future__ import absolute_import, division, print_function, unicode_litera
 import datetime as dt
 import time
 
-from scout_apm.compat import datetime_to_timestamp, parse_qsl, urlencode
+from scout_apm.compat import datetime_to_timestamp, parse_qsl, text_type, urlencode
 from scout_apm.core.config import scout_config
 
 # Originally derived from:
@@ -43,6 +43,15 @@ FILTER_PARAMETERS = frozenset(
 def create_filtered_path(path, query_params):
     if scout_config.value("uri_reporting") == "path":
         return path
+    # We expect query_params to have keys and values both of strings, because
+    # that's how frameworks build it. However sometimes application code
+    # mutates this structure to use incorrect types before we read it, so we
+    # have to cautiously make everything a string again. Ignoring the
+    # possibilities of bytes or objects with bad __str__ methods because they
+    # seem very unlikely.
+    string_query_params = (
+        (text_type(key), text_type(value)) for key, value in query_params
+    )
     # Python 2 unicode compatibility: force all keys and values to bytes
     filtered_params = sorted(
         (
@@ -54,7 +63,7 @@ def create_filtered_path(path, query_params):
                     else value.encode("utf-8")
                 ),
             )
-            for key, value in query_params
+            for key, value in string_query_params
         )
     )
     if not filtered_params:


Test patch:
diff --git a/tests/unit/core/test_web_requests.py b/tests/unit/core/test_web_requests.py
index cf76ce7..c756559 100644
--- a/tests/unit/core/test_web_requests.py
+++ b/tests/unit/core/test_web_requests.py
@@ -35,6 +35,10 @@ from scout_apm.core.web_requests import (
             [("password", "hunter2"), ("password", "hunter3")],
             "/?password=%5BFILTERED%5D&password=%5BFILTERED%5D",
         ),
+        # Check that if a user mutates the query params to contain non-string
+        # types, we cast them to strings.
+        ("/", [("bar", 1)], "/?bar=1"),
+        ("/", [("bar", None)], "/?bar=None"),
     ],
 )
 def test_create_filtered_path(path, params, expected):