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')>, [])
```



Discussion:
codecov[bot]: # [Codecov](https://app.codecov.io/gh/getsentry/responses/compare/507?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry) Report
> Merging [#507](https://app.codecov.io/gh/getsentry/responses/compare/507?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry) (59f6254) into [master](https://codecov.io/gh/getsentry/responses/commit/a35896e9a1690730524c68d689ff1fde269d797a?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry) (a35896e) will **not change** coverage.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/getsentry/responses/pull/507/graphs/tree.svg?width=650&height=150&src=pr&token=x2tAGR7rxE&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry)](https://app.codecov.io/gh/getsentry/responses/compare/507?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry)

```diff
@@            Coverage Diff             @@
##            master      #507    +/-   ##
==========================================
  Coverage   100.00%   100.00%            
==========================================
  Files            6         6            
  Lines         2200      2091   -109     
==========================================
- Hits          2200      2091   -109     
```


| [Impacted Files](https://app.codecov.io/gh/getsentry/responses/compare/507?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry) | Coverage Δ | |
|---|---|---|
| [responses/\_\_init\_\_.py](https://app.codecov.io/gh/getsentry/responses/compare/507/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry#diff-cmVzcG9uc2VzL19faW5pdF9fLnB5) | `100.00% <100.00%> (ø)` | |
| [responses/test\_responses.py](https://app.codecov.io/gh/getsentry/responses/compare/507/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry#diff-cmVzcG9uc2VzL3Rlc3RfcmVzcG9uc2VzLnB5) | `100.00% <100.00%> (ø)` | |
| [responses/test\_matchers.py](https://app.codecov.io/gh/getsentry/responses/compare/507/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry#diff-cmVzcG9uc2VzL3Rlc3RfbWF0Y2hlcnMucHk=) | `100.00% <0.00%> (ø)` | |
| [responses/test\_registries.py](https://app.codecov.io/gh/getsentry/responses/compare/507/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry#diff-cmVzcG9uc2VzL3Rlc3RfcmVnaXN0cmllcy5weQ==) | `100.00% <0.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://app.codecov.io/gh/getsentry/responses/compare/507?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://app.codecov.io/gh/getsentry/responses/compare/507?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry). Last update [a35896e...59f6254](https://app.codecov.io/gh/getsentry/responses/compare/507?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry).


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()