<p dir="auto">Using python 3.3 on Mac OS:</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="import requests
requests.post(&quot;http://localhost:8080/nonexistent&quot;,
              headers = {'Content-Type': 'text/plain;charset=UTF-8'},
              data=b'foo')"><pre class="notranslate"><code class="notranslate">import requests
requests.post("http://localhost:8080/nonexistent",
              headers = {'Content-Type': 'text/plain;charset=UTF-8'},
              data=b'foo')
</code></pre></div>
<p dir="auto">results in the following request (captured with tcpdump):</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="POST /nonexistent HTTP/1.1
Host: localhost:8080
Accept-Encoding: identity
Accept-Encoding: gzip, deflate, compress
Content-Type: application/x-www-form-urlencoded
Content-Type: text/plain;charset=UTF-8
Accept: */*
Content-Length: 3
User-Agent: python-requests/1.2.0 CPython/3.3.1 Darwin/12.3.0

foo"><pre class="notranslate"><code class="notranslate">POST /nonexistent HTTP/1.1
Host: localhost:8080
Accept-Encoding: identity
Accept-Encoding: gzip, deflate, compress
Content-Type: application/x-www-form-urlencoded
Content-Type: text/plain;charset=UTF-8
Accept: */*
Content-Length: 3
User-Agent: python-requests/1.2.0 CPython/3.3.1 Darwin/12.3.0

foo
</code></pre></div>
<p dir="auto">Note that the specified content-type header is present, but another "Content-Type: application/x-www-form-urlencoded" is added. As far as I can tell, the extra Content-Type header is added at the end of prepare_body in models.py due to  <code class="notranslate">'content-type' in self.headers</code> not recognizing the existing content-type header in the <code class="notranslate">self.headers</code> dict (the key is, at that time, <code class="notranslate">b'Content-type</code>, and it seems it's this str &lt;-&gt; bytes comparison that fails).</p>
<p dir="auto">It is possible to work around this by wrapping the bytes in a BytesIO object (and setting it's len property), but it still seems like a bug?</p>
<p dir="auto"><a href="https://tools.ietf.org/html/rfc6874" rel="nofollow">RFC6874 </a>updates <a href="https://tools.ietf.org/html/rfc3986" rel="nofollow">RFC3986 </a> URI Syntax to support IPv6 Zone Identifiers. Zone identifiers are required when working with link local IPv6 addresses on a machine with multiple network adapters.</p>
<p dir="auto">RFC6874 updates the URI ABNF to include:</p>
<blockquote>
<p dir="auto">IP-literal = "[" ( IPv6address / IPv6addrz / IPvFuture  ) "]"<br>
ZoneID = 1*( unreserved / pct-encoded )<br>
IPv6addrz = IPv6address "%25" ZoneID</p>
</blockquote>
<p dir="auto">Windows systems use integers for the zone identifiers (Linux systems often use interface names instead).</p>
<p dir="auto">When performing a get request to a link local address with zone ID "37" formatted as per RFC6874 (e.g. http://[fe80::1234%2537]) the percent encoding seems to be interpreted incorrectly and attempts to access fe80::12347 which is an invalid IPv6 address.</p>
<h2 dir="auto">Expected Result</h2>
<p dir="auto">Get request to "fe80::1234%37" performed correctly</p>
<h2 dir="auto">Actual Result</h2>
<p dir="auto">urlllib3 LocationParseError and requests.exceptions.InvalidURL exceptions are generated.</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="&gt;&gt;&gt; r = requests.get(&quot;http://[fe80::1234%2537]&quot;)
Traceback (most recent call last):
  File &quot;C:\Users\username_removed\AppData\Local\Programs\Python\Python39\lib\site-packages\requests\adapters.py&quot;, line 412, in send
    conn = self.get_connection(request.url, proxies)
  File &quot;C:\Users\username_removed\AppData\Local\Programs\Python\Python39\lib\site-packages\requests\adapters.py&quot;, line 315, in get_connection
    conn = self.poolmanager.connection_from_url(url)
  File &quot;C:\Users\username_removed\AppData\Local\Programs\Python\Python39\lib\site-packages\urllib3\poolmanager.py&quot;, line 297, in connection_from_url
    u = parse_url(url)
  File &quot;C:\Users\username_removed\AppData\Local\Programs\Python\Python39\lib\site-packages\urllib3\util\url.py&quot;, line 392, in parse_url
    return six.raise_from(LocationParseError(source_url), None)
  File &quot;&lt;string&gt;&quot;, line 3, in raise_from
urllib3.exceptions.LocationParseError: Failed to parse: http://[fe80::12347]/

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File &quot;&lt;stdin&gt;&quot;, line 1, in &lt;module&gt;
  File &quot;C:\Users\username_removed\AppData\Local\Programs\Python\Python39\lib\site-packages\requests\api.py&quot;, line 76, in get
    return request('get', url, params=params, **kwargs)
  File &quot;C:\Users\username_removed\AppData\Local\Programs\Python\Python39\lib\site-packages\requests\api.py&quot;, line 61, in request
    return session.request(method=method, url=url, **kwargs)
  File &quot;C:\Users\username_removed\AppData\Local\Programs\Python\Python39\lib\site-packages\requests\sessions.py&quot;, line 542, in request
    resp = self.send(prep, **send_kwargs)
  File &quot;C:\Users\username_removed\AppData\Local\Programs\Python\Python39\lib\site-packages\requests\sessions.py&quot;, line 655, in send
    r = adapter.send(request, **kwargs)
  File &quot;C:\Users\username_removed\AppData\Local\Programs\Python\Python39\lib\site-packages\requests\adapters.py&quot;, line 414, in send
    raise InvalidURL(e, request=request)
requests.exceptions.InvalidURL: Failed to parse: http://[fe80::12347]/"><pre class="notranslate"><code class="notranslate">&gt;&gt;&gt; r = requests.get("http://[fe80::1234%2537]")
Traceback (most recent call last):
  File "C:\Users\username_removed\AppData\Local\Programs\Python\Python39\lib\site-packages\requests\adapters.py", line 412, in send
    conn = self.get_connection(request.url, proxies)
  File "C:\Users\username_removed\AppData\Local\Programs\Python\Python39\lib\site-packages\requests\adapters.py", line 315, in get_connection
    conn = self.poolmanager.connection_from_url(url)
  File "C:\Users\username_removed\AppData\Local\Programs\Python\Python39\lib\site-packages\urllib3\poolmanager.py", line 297, in connection_from_url
    u = parse_url(url)
  File "C:\Users\username_removed\AppData\Local\Programs\Python\Python39\lib\site-packages\urllib3\util\url.py", line 392, in parse_url
    return six.raise_from(LocationParseError(source_url), None)
  File "&lt;string&gt;", line 3, in raise_from
urllib3.exceptions.LocationParseError: Failed to parse: http://[fe80::12347]/

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "&lt;stdin&gt;", line 1, in &lt;module&gt;
  File "C:\Users\username_removed\AppData\Local\Programs\Python\Python39\lib\site-packages\requests\api.py", line 76, in get
    return request('get', url, params=params, **kwargs)
  File "C:\Users\username_removed\AppData\Local\Programs\Python\Python39\lib\site-packages\requests\api.py", line 61, in request
    return session.request(method=method, url=url, **kwargs)
  File "C:\Users\username_removed\AppData\Local\Programs\Python\Python39\lib\site-packages\requests\sessions.py", line 542, in request
    resp = self.send(prep, **send_kwargs)
  File "C:\Users\username_removed\AppData\Local\Programs\Python\Python39\lib\site-packages\requests\sessions.py", line 655, in send
    r = adapter.send(request, **kwargs)
  File "C:\Users\username_removed\AppData\Local\Programs\Python\Python39\lib\site-packages\requests\adapters.py", line 414, in send
    raise InvalidURL(e, request=request)
requests.exceptions.InvalidURL: Failed to parse: http://[fe80::12347]/
</code></pre></div>
<p dir="auto">It seems that part of the zone ID is concatenated onto the IPv6 address and the % removed altogther.</p>
<h2 dir="auto">Reproduction Steps</h2>
<div class="highlight highlight-source-python notranslate position-relative overflow-auto" dir="auto" data-snippet-clipboard-copy-content="import requests
r = requests.get(&quot;http://[fe80::1234%2537]&quot;)"><pre class="notranslate"><span class="pl-k">import</span> <span class="pl-s1">requests</span>
<span class="pl-s1">r</span> <span class="pl-c1">=</span> <span class="pl-s1">requests</span>.<span class="pl-en">get</span>(<span class="pl-s">"http://[fe80::1234%2537]"</span>)</pre></div>
<p dir="auto">Note that the zone identifiers vary between PCs/OSs and sometimes over reboots, it is unlikely that 37 will be a valid Zone ID on most systems. However the connection should fail with "[WinError 10049] The requested address is not valid in its context" rather than</p>
<h2 dir="auto">Other Notes</h2>
<p dir="auto">The correct URL can be accesses by escaping the Zone ID twice. e.g:</p>
<div class="highlight highlight-source-python notranslate position-relative overflow-auto" dir="auto" data-snippet-clipboard-copy-content="import requests
r = requests.get(&quot;http://[fe80::1234%252537]&quot;)"><pre class="notranslate"><span class="pl-k">import</span> <span class="pl-s1">requests</span>
<span class="pl-s1">r</span> <span class="pl-c1">=</span> <span class="pl-s1">requests</span>.<span class="pl-en">get</span>(<span class="pl-s">"http://[fe80::1234%252537]"</span>)</pre></div>
<p dir="auto">Will attempt to access the correct address fe80::1234%37 perhaps %encoding is being performed twice?</p>
<p dir="auto">Exact behaviour depends on the specific zone ID used, for instance some zone IDs work with or without the percent encoded % character:</p>
<ul dir="auto">
<li>r = requests.get("http://[fe80::1234%1]")</li>
<li>r = requests.get("http://[fe80::1234%251]")</li>
<li>r = requests.get("http://[fe80::1234%25251]")</li>
</ul>
<p dir="auto">All attempt a get request from fe80::1234%1</p>
<h2 dir="auto">urllib3 behaviour</h2>
<p dir="auto">urllib3 seems to perform in accordance with RFC6874:</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="&gt;&gt;&gt; import urllib3
&gt;&gt;&gt; urllib3.util.parse_url(&quot;http://[fe80::1234%2537]&quot;)
Url(scheme='http', auth=None, host='[fe80::1234%37]', port=None, path=None, query=None, fragment=None)"><pre class="notranslate"><code class="notranslate">&gt;&gt;&gt; import urllib3
&gt;&gt;&gt; urllib3.util.parse_url("http://[fe80::1234%2537]")
Url(scheme='http', auth=None, host='[fe80::1234%37]', port=None, path=None, query=None, fragment=None)
</code></pre></div>
<h2 dir="auto">System Information</h2>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="$ python -m requests.help"><pre class="notranslate"><code class="notranslate">$ python -m requests.help
</code></pre></div>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="{
  &quot;chardet&quot;: {
    &quot;version&quot;: &quot;4.0.0&quot;
  },
  &quot;cryptography&quot;: {
    &quot;version&quot;: &quot;&quot;
  },
  &quot;idna&quot;: {
    &quot;version&quot;: &quot;2.10&quot;
  },
  &quot;implementation&quot;: {
    &quot;name&quot;: &quot;CPython&quot;,
    &quot;version&quot;: &quot;3.9.2&quot;
  },
  &quot;platform&quot;: {
    &quot;release&quot;: &quot;10&quot;,
    &quot;system&quot;: &quot;Windows&quot;
  },
  &quot;pyOpenSSL&quot;: {
    &quot;openssl_version&quot;: &quot;&quot;,
    &quot;version&quot;: null
  },
  &quot;requests&quot;: {
    &quot;version&quot;: &quot;2.25.1&quot;
  },
  &quot;system_ssl&quot;: {
    &quot;version&quot;: &quot;1010109f&quot;
  },
  &quot;urllib3&quot;: {
    &quot;version&quot;: &quot;1.26.3&quot;
  },
  &quot;using_pyopenssl&quot;: false
}"><pre class="notranslate"><code class="notranslate">{
  "chardet": {
    "version": "4.0.0"
  },
  "cryptography": {
    "version": ""
  },
  "idna": {
    "version": "2.10"
  },
  "implementation": {
    "name": "CPython",
    "version": "3.9.2"
  },
  "platform": {
    "release": "10",
    "system": "Windows"
  },
  "pyOpenSSL": {
    "openssl_version": "",
    "version": null
  },
  "requests": {
    "version": "2.25.1"
  },
  "system_ssl": {
    "version": "1010109f"
  },
  "urllib3": {
    "version": "1.26.3"
  },
  "using_pyopenssl": false
}
</code></pre></div>