codecov[bot]: # [Codecov](https://codecov.io/gh/getsentry/responses/pull/176?src=pr&el=h1) Report
> Merging [#176](https://codecov.io/gh/getsentry/responses/pull/176?src=pr&el=desc) into [master](https://codecov.io/gh/getsentry/responses/commit/92b2d4597e03c8dee096329fca2ed7f28c8fefa5?src=pr&el=desc) will **increase** coverage by `0.75%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/getsentry/responses/pull/176/graphs/tree.svg?token=x2tAGR7rxE&width=650&height=150&src=pr)](https://codecov.io/gh/getsentry/responses/pull/176?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #176      +/-   ##
==========================================
+ Coverage   95.06%   95.81%   +0.75%     
==========================================
  Files           1        1              
  Lines         304      311       +7     
==========================================
+ Hits          289      298       +9     
+ Misses         15       13       -2
```


| [Impacted Files](https://codecov.io/gh/getsentry/responses/pull/176?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [responses.py](https://codecov.io/gh/getsentry/responses/pull/176?src=pr&el=tree#diff-cmVzcG9uc2VzLnB5) | `95.81% <100%> (+0.75%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/getsentry/responses/pull/176?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/getsentry/responses/pull/176?src=pr&el=footer). Last update [92b2d45...9a3cfd3](https://codecov.io/gh/getsentry/responses/pull/176?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

diff --git a/responses.py b/responses.py
index c2a3280..1c48310 100644
--- a/responses.py
+++ b/responses.py
@@ -23,8 +23,9 @@ except ImportError:
 
 if six.PY2:
     from urlparse import urlparse, parse_qsl, urlsplit, urlunsplit
+    from urllib import quote
 else:
-    from urllib.parse import urlparse, parse_qsl, urlsplit, urlunsplit
+    from urllib.parse import urlparse, parse_qsl, urlsplit, urlunsplit, quote
 
 if six.PY2:
     try:
@@ -53,6 +54,21 @@ def _is_string(s):
     return isinstance(s, six.string_types)
 
 
+def _has_unicode(s):
+    return any(ord(char) > 128 for char in s)
+
+
+def _clean_unicode(url):
+    if isinstance(url.encode('utf8'), six.string_types):
+        url = url.encode('utf8')
+    chars = list(url)
+    for i, x in enumerate(chars):
+        if ord(x) > 128:
+            chars[i] = quote(x)
+
+    return ''.join(chars)
+
+
 def _is_redirect(response):
     try:
         # 2.0.0 <= requests <= 2.2
@@ -173,22 +189,18 @@ class BaseResponse(object):
             return False
 
         for (a_k, a_v), (b_k, b_v) in zip(url_qsl, other_qsl):
-            if not isinstance(a_k, six.text_type):
-                a_k = a_k.decode('utf-8')
-            if not isinstance(b_k, six.text_type):
-                b_k = b_k.decode('utf-8')
             if a_k != b_k:
                 return False
-            if not isinstance(a_v, six.text_type):
-                a_v = a_v.decode('utf-8')
-            if not isinstance(b_v, six.text_type):
-                b_v = b_v.decode('utf-8')
             if a_v != b_v:
                 return False
         return True
 
     def _url_matches(self, url, other, match_querystring=False):
         if _is_string(url):
+            if _has_unicode(url):
+                url = _clean_unicode(url)
+                if not isinstance(other, six.text_type):
+                    other = other.encode('ascii').decode('utf8')
             if match_querystring:
                 return self._url_matches_strict(url, other)
             else:

Issues with Unicode in path
I saw that you fixed #153 by handling Unicode characters in the querystring, but there still appear to be issues when said Unicode characters are in the URL path itself. If you pass a mock URL with a Unicode character in it to Responses and then request the same URL via Requests, you get a connection refused error.

[This is the line](https://github.com/getsentry/responses/blob/master/responses.py#L166) where it fails, when I stepped through the debugger.

I'm using Python 2.7.13 and Responses 0.8.0. Please let me know if I need to clarify this issue. Thanks!
diff --git a/test_responses.py b/test_responses.py
index 6965a1c..a7c3ed2 100644
--- a/test_responses.py
+++ b/test_responses.py
@@ -648,7 +648,7 @@ def test_allow_redirects_samehost():
     assert_reset()
 
 
-def test_handles_chinese_url():
+def test_handles_unicode_querystring():
     url = u'http://example.com/test?type=2&ie=utf8&query=汉字'
 
     @responses.activate
@@ -663,6 +663,21 @@ def test_handles_chinese_url():
     assert_reset()
 
 
+def test_handles_unicode_url():
+    url = u'https://hi.wikipedia.org/wiki/दिलवाले_दुल्हनिया_ले_जाएंगे'
+
+    @responses.activate
+    def run():
+        responses.add(responses.GET, url, body='test')
+
+        resp = requests.get(url)
+
+        assert_response(resp, 'test')
+
+    run()
+    assert_reset()
+
+
 def test_headers():
     @responses.activate
     def run():

pip install -e .[tests]
parse_log_pytest
requirements.txt
pytest pytest-cov pytest-localserver flake8
apt-get update apt-get install -y gcc
3.9
requirements/base.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
certifi==2025.1.31
charset-normalizer==3.4.1
cookies==2.2.1
coverage==4.5.4
exceptiongroup==1.2.2
flake8==7.2.0
idna==3.10
iniconfig==2.1.0
MarkupSafe==3.0.2
mccabe==0.7.0
packaging==24.2
pluggy==1.5.0
pycodestyle==2.13.0
pyflakes==3.3.2
pytest==8.3.5
pytest-cov==2.10.1
pytest-localserver==0.9.0.post0
requests==2.32.3
-e git+https://github.com/getsentry/responses.git@92b2d4597e03c8dee096329fca2ed7f28c8fefa5#egg=responses
six==1.17.0
tomli==2.2.1
urllib3==2.3.0
Werkzeug==3.1.3

name: responses
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - certifi==2025.1.31
      - charset-normalizer==3.4.1
      - cookies==2.2.1
      - coverage==4.5.4
      - exceptiongroup==1.2.2
      - flake8==7.2.0
      - idna==3.10
      - iniconfig==2.1.0
      - markupsafe==3.0.2
      - mccabe==0.7.0
      - packaging==24.2
      - pluggy==1.5.0
      - pycodestyle==2.13.0
      - pyflakes==3.3.2
      - pytest==8.3.5
      - pytest-cov==2.10.1
      - pytest-localserver==0.9.0.post0
      - requests==2.32.3
      - six==1.17.0
      - tomli==2.2.1
      - urllib3==2.3.0
      - werkzeug==3.1.3
prefix: /opt/conda/envs/responses

test_responses.py::test_handles_unicode_url
test_responses.py::test_replace[http://example.com/two-http://example.com/two] test_responses.py::test_replace[original1-replacement1] test_responses.py::test_replace[http://example\\.com/two-http://example\\.com/two] test_responses.py::test_replace_error[http://example.com/one-http://example\\.com/one] test_responses.py::test_replace_error[http://example\\.com/one-http://example.com/one] test_responses.py::test_remove test_responses.py::test_response_equality[args10-kwargs10-args20-kwargs20-True] test_responses.py::test_response_equality[args11-kwargs11-args21-kwargs21-False] test_responses.py::test_response_equality[args13-kwargs13-args23-kwargs23-True] test_responses.py::test_match_querystring_regex test_responses.py::test_match_querystring_error_regex test_responses.py::test_arbitrary_status_code test_responses.py::test_regular_expression_url test_responses.py::test_allow_redirects_samehost
test_responses.py::test_response test_responses.py::test_response_with_instance test_responses.py::test_response_equality[args12-kwargs12-args22-kwargs22-False] test_responses.py::test_response_equality_different_objects test_responses.py::test_connection_error test_responses.py::test_match_querystring test_responses.py::test_match_empty_querystring test_responses.py::test_match_querystring_error test_responses.py::test_accept_string_body test_responses.py::test_accept_json_body test_responses.py::test_no_content_type test_responses.py::test_throw_connection_error_explicit test_responses.py::test_callback test_responses.py::test_callback_no_content_type test_responses.py::test_custom_adapter test_responses.py::test_responses_as_context_manager test_responses.py::test_activate_doesnt_change_signature test_responses.py::test_activate_doesnt_change_signature_for_method test_responses.py::test_response_cookies test_responses.py::test_response_callback test_responses.py::test_response_filebody test_responses.py::test_assert_all_requests_are_fired test_responses.py::test_handles_unicode_querystring test_responses.py::test_headers test_responses.py::test_legacy_adding_headers test_responses.py::test_multiple_responses test_responses.py::test_multiple_urls test_responses.py::test_passthru test_responses.py::test_method_named_param
responses.py
responses.py