Repository: getsentry/responses

Issue:
Path segment parameters not handled correctly
URL matching does not respect path segment parameters in the last path segment. This is probably caused by using `urlparse` instead of `urlsplit` in [_get_url_and_path](https://github.com/getsentry/responses/blob/a35896e9a1690730524c68d689ff1fde269d797a/responses/__init__.py#L215).

### Environment

Python 3.9
responses 0.18.0

### Steps to Reproduce

```
import responses
import requests

mock = responses.RequestsMock(assert_all_requests_are_fired=False)

mock.add(responses.GET, 'http://example.com/here/we')
mock.add(responses.GET, 'http://example.com/here/we;go')

request = requests.Request('GET', 'http://example.com/here/we;go').prepare()
print(mock._registry.find(request))
```

### Expected Result

Match the second mocked `CallbackResponse` with path segment parameter `go`.

### Actual Result

```
(<Response(url='http://example.com/here/we' status=200 content_type='text/plain' headers='null')>, [])
```



Comments:

Patch:
diff --git a/CHANGES b/CHANGES
index 6f3cc30..39b71e6 100644
--- a/CHANGES
+++ b/CHANGES
@@ -10,7 +10,11 @@
 * Add support for the `loose` version of `query_param_matcher` via named argument `strict_match`.
 * Added support for `async/await` functions.
 * `response_callback` is no longer executed on exceptions raised by failed `Response`s
+* Change logic of `_get_url_and_path` to comply with RFC 3986. Now URL match occurs by matching
+  schema, authority and path, where path is terminated by the first question mark ("?") or
+  number sign ("#") character, or by the end of the URI.
 * An error is now raised when both `content_type` and `headers[content-type]` are provided as parameters.
+* When a request isn't matched the passthru prefixes are now included in error messages.
 
 
 0.18.0
diff --git a/responses/__init__.py b/responses/__init__.py
index 70c6260..94e0fae 100644
--- a/responses/__init__.py
+++ b/responses/__init__.py
@@ -38,7 +38,6 @@ from io import BytesIO
 from unittest import mock as std_mock
 from urllib.parse import parse_qsl
 from urllib.parse import quote
-from urllib.parse import urlparse
 from urllib.parse import urlsplit
 from urllib.parse import urlunparse
 from urllib.parse import urlunsplit
@@ -213,7 +212,7 @@ def _ensure_url_default_path(url):
 
 
 def _get_url_and_path(url):
-    url_parsed = urlparse(url)
+    url_parsed = urlsplit(url)
     url_and_path = urlunparse(
         [url_parsed.scheme, url_parsed.netloc, url_parsed.path, None, None, None]
     )
@@ -269,7 +268,7 @@ class BaseResponse(object):
         self.url = _ensure_url_default_path(url)
 
         if self._should_match_querystring(match_querystring):
-            match = tuple(match) + (_query_string_matcher(urlparse(self.url).query),)
+            match = tuple(match) + (_query_string_matcher(urlsplit(self.url).query),)
 
         self.match = match
         self.call_count = 0
@@ -309,7 +308,7 @@ class BaseResponse(object):
                 )
             return match_querystring_argument
 
-        return bool(urlparse(self.url).query)
+        return bool(urlsplit(self.url).query)
 
     def _url_matches(self, url, other):
         if isinstance(url, str):
@@ -744,7 +743,7 @@ class RequestsMock(object):
 
     def _parse_request_params(self, url):
         params = {}
-        for key, val in groupby(parse_qsl(urlparse(url).query), lambda kv: kv[0]):
+        for key, val in groupby(parse_qsl(urlsplit(url).query), lambda kv: kv[0]):
             values = list(map(lambda x: x[1], val))
             if len(values) == 1:
                 values = values[0]
@@ -784,6 +783,11 @@ class RequestsMock(object):
                     m.method, m.url, match_failed_reasons[i]
                 )
 
+            if self.passthru_prefixes:
+                error_msg += "Passthru prefixes:\n"
+                for p in self.passthru_prefixes:
+                    error_msg += "- {}\n".format(p)
+
             response = ConnectionError(error_msg)
             response.request = request
 


Test patch:
diff --git a/responses/test_responses.py b/responses/test_responses.py
index 3ddca9b..a656b83 100644
--- a/responses/test_responses.py
+++ b/responses/test_responses.py
@@ -1158,6 +1158,28 @@ def test_allow_redirects_samehost():
     assert_reset()
 
 
+def test_path_segments():
+    """Test that path segment after ``;`` is preserved.
+
+    Validate compliance with RFC 3986.
+    The path is terminated by the first question mark ("?") or
+    number sign ("#") character, or by the end of the URI.
+    See more about how path should be treated under:
+    https://datatracker.ietf.org/doc/html/rfc3986.html#section-3.3
+    """
+
+    @responses.activate
+    def run():
+        responses.add(responses.GET, "http://example.com/here/we", status=669)
+        responses.add(responses.GET, "http://example.com/here/we;go", status=777)
+
+        resp = requests.get("http://example.com/here/we;go")
+        assert resp.status_code == 777
+
+    run()
+    assert_reset()
+
+
 def test_handles_unicode_querystring():
     url = "http://example.com/test?type=2&ie=utf8&query=汉字"
 
@@ -1817,6 +1839,7 @@ def test_fail_request_error():
         with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
             rsps.add("POST", "http://example1.com")
             rsps.add("GET", "http://example.com")
+            rsps.add_passthru("http://other.example.com")
 
             with pytest.raises(ConnectionError) as excinfo:
                 requests.post("http://example.com", data={"id": "bad"})
@@ -1824,6 +1847,7 @@ def test_fail_request_error():
             msg = str(excinfo.value)
             assert "- POST http://example1.com/ URL does not match" in msg
             assert "- GET http://example.com/ Method does not match" in msg
+            assert "Passthru prefixes:\n- http://other.example.com" in msg
 
     run()
     assert_reset()