<p dir="auto">I want to start off by saying I think this is a pretty unusual use case and it appears to me to boil down to a behaviour in <code class="notranslate">urllib3</code>. However, I encountered it while working with <code class="notranslate">requests</code> and I'm not familiar enough with <code class="notranslate">urllib3</code> to say whether the behaviour is expected or not. It may be that the fix is to document this behaviour in <code class="notranslate">requests</code>, or make a change in <code class="notranslate">urllib3</code>, but I don't know for sure.</p>
<h4 dir="auto">Problem Summary</h4>
<ol dir="auto">
<li>Configure a <code class="notranslate">requests.sessions.Session</code> with a CA file.</li>
<li>Make a request using HTTPS to a host that has a certificate signed by this CA.</li>
<li>Reconfigure the <code class="notranslate">requests.sessions.Session</code> object with a <em>new</em> CA file and delete the old one.</li>
<li>Wait until the connection with the server is closed (either from inactivity or by forcing it with 'Connection: close' on the request made in step 2).</li>
<li>Make a second request using HTTPS to a host that has a certificate signed by the new CA.</li>
<li>Instead of the same response from step 2, an exception is raised.</li>
</ol>
<p dir="auto">The exception looks something like this and is from the reproducer I've included below:</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="Traceback (most recent call last):
  File &quot;test_requests.py&quot;, line 78, in &lt;module&gt;
    headers={'Connection': 'close'})
  File &quot;/home/jcline/.virtualenvs/test_requests/lib/python2.7/site-packages/requests/sessions.py&quot;, line 480, in get
    return self.request('GET', url, **kwargs)
  File &quot;/home/jcline/.virtualenvs/test_requests/lib/python2.7/site-packages/requests/sessions.py&quot;, line 468, in request
    resp = self.send(prep, **send_kwargs)
  File &quot;/home/jcline/.virtualenvs/test_requests/lib/python2.7/site-packages/requests/sessions.py&quot;, line 576, in send
    r = adapter.send(request, **kwargs)
  File &quot;/home/jcline/.virtualenvs/test_requests/lib/python2.7/site-packages/requests/adapters.py&quot;, line 447, in send
    raise SSLError(e, request=request)
requests.exceptions.SSLError: [Errno 2] No such file or directory"><pre class="notranslate"><code class="notranslate">Traceback (most recent call last):
  File "test_requests.py", line 78, in &lt;module&gt;
    headers={'Connection': 'close'})
  File "/home/jcline/.virtualenvs/test_requests/lib/python2.7/site-packages/requests/sessions.py", line 480, in get
    return self.request('GET', url, **kwargs)
  File "/home/jcline/.virtualenvs/test_requests/lib/python2.7/site-packages/requests/sessions.py", line 468, in request
    resp = self.send(prep, **send_kwargs)
  File "/home/jcline/.virtualenvs/test_requests/lib/python2.7/site-packages/requests/sessions.py", line 576, in send
    r = adapter.send(request, **kwargs)
  File "/home/jcline/.virtualenvs/test_requests/lib/python2.7/site-packages/requests/adapters.py", line 447, in send
    raise SSLError(e, request=request)
requests.exceptions.SSLError: [Errno 2] No such file or directory
</code></pre></div>
<p dir="auto">I did some digging and what I concluded is that a when a connection is pulled out of the <code class="notranslate">urllib3</code> connection pool the CA certificates (maybe even the client certificates?) are not reconfigured on the connection object, so when it goes to validate the server's certificate when it restarts the TCP connection it gets that "No such file or directory" when it tries to read that old CA certificate file.</p>
<p dir="auto">I thought about this a bit and it may be the case that if you reconfigure the session object with a new CA file and don't delete the old one, you might encounter odd cases where a certificate you expect to pass validation with the new CA bundle fails because the old bundle is actually being used on some of the connections in the pool (but possibly not all of them).</p>
<h4 dir="auto">Environment</h4>
<p dir="auto">I encountered this on Fedora 23. I installed requests into a clean virtualenv with pip rather than using the distro-provided packages. I saw this with requests-2.9.1.</p>
<h4 dir="auto">Reproducer</h4>
<p dir="auto">I've written up a little script that reproduces this problem. It uses the same CA for both requests, but they are in different files and the CA file used for the first request is deleted.</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="import os
import tempfile

import requests


# The CA that signed the certificate used at cdn.redhat.com
CA = &quot;&quot;&quot;
-----BEGIN CERTIFICATE-----
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
Kg6Ay0rrsi4nw1JHue2GvdjdfVOaWSWC
-----END CERTIFICATE-----
&quot;&quot;&quot;

# Write two versions of the CA to temporary files. ca1 is used for the first request
# and ca2 is used for the second (after ca1 has been deleted).
fd, ca1_abs_path = tempfile.mkstemp(dir='/tmp/', text=True)
os.write(fd, CA)
os.close(fd)
fd, ca2_abs_path = tempfile.mkstemp(dir='/tmp/', text=True)
os.write(fd, CA)
os.close(fd)

# Make an initial request with the first CA
session = requests.sessions.Session()
session.verify = ca1_abs_path
response = session.get('https://cdn.redhat.com/',
                       headers={'Connection': 'close'})
print('Got HTTP ' + str(response.status_code) + ' (403 expected)')

# Remove the first CA and configure the session to use the second one.
os.remove(ca1_abs_path)
session.verify = ca2_abs_path

# This is going to end in a &quot;file not found&quot; because the connection needs to be
# re-established by urllib3 and the certificate on the connection is not
# updated to match the certificate on the connection pool.
try:
    response = session.get('https://cdn.redhat.com/',
                           headers={'Connection': 'close'})
    print('Got HTTP ' + str(response.status_code) + ' (403 expected)')
except requests.exceptions.SSLError as e:
    os.remove(ca2_abs_path)
    raise"><pre class="notranslate"><code class="notranslate">import os
import tempfile

import requests


# The CA that signed the certificate used at cdn.redhat.com
CA = """
-----BEGIN CERTIFICATE-----
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
Kg6Ay0rrsi4nw1JHue2GvdjdfVOaWSWC
-----END CERTIFICATE-----
"""

# Write two versions of the CA to temporary files. ca1 is used for the first request
# and ca2 is used for the second (after ca1 has been deleted).
fd, ca1_abs_path = tempfile.mkstemp(dir='/tmp/', text=True)
os.write(fd, CA)
os.close(fd)
fd, ca2_abs_path = tempfile.mkstemp(dir='/tmp/', text=True)
os.write(fd, CA)
os.close(fd)

# Make an initial request with the first CA
session = requests.sessions.Session()
session.verify = ca1_abs_path
response = session.get('https://cdn.redhat.com/',
                       headers={'Connection': 'close'})
print('Got HTTP ' + str(response.status_code) + ' (403 expected)')

# Remove the first CA and configure the session to use the second one.
os.remove(ca1_abs_path)
session.verify = ca2_abs_path

# This is going to end in a "file not found" because the connection needs to be
# re-established by urllib3 and the certificate on the connection is not
# updated to match the certificate on the connection pool.
try:
    response = session.get('https://cdn.redhat.com/',
                           headers={'Connection': 'close'})
    print('Got HTTP ' + str(response.status_code) + ' (403 expected)')
except requests.exceptions.SSLError as e:
    os.remove(ca2_abs_path)
    raise
</code></pre></div>
<p dir="auto">Using this code:</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="import requests
requests.get('https://github.com')"><pre class="notranslate"><code class="notranslate">import requests
requests.get('https://github.com')
</code></pre></div>
<p dir="auto">Throws the following exception:</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="Traceback (most recent call last):
  File &quot;&lt;stdin&gt;&quot;, line 1, in &lt;module&gt;
  File &quot;/usr/lib/python2.7/site-packages/requests/api.py&quot;, line 55, in get
    return request('get', url, **kwargs)
  File &quot;/usr/lib/python2.7/site-packages/requests/api.py&quot;, line 44, in request
    return session.request(method=method, url=url, **kwargs)
  File &quot;/usr/lib/python2.7/site-packages/requests/sessions.py&quot;, line 456, in request
    resp = self.send(prep, **send_kwargs)
  File &quot;/usr/lib/python2.7/site-packages/requests/sessions.py&quot;, line 559, in send
    r = adapter.send(request, **kwargs)
  File &quot;/usr/lib/python2.7/site-packages/requests/adapters.py&quot;, line 382, in send
    raise SSLError(e, request=request)
requests.exceptions.SSLError: hostname '&lt;Proxy&gt;' doesn't match either of 'github.com', 'www.github.com'"><pre class="notranslate"><code class="notranslate">Traceback (most recent call last):
  File "&lt;stdin&gt;", line 1, in &lt;module&gt;
  File "/usr/lib/python2.7/site-packages/requests/api.py", line 55, in get
    return request('get', url, **kwargs)
  File "/usr/lib/python2.7/site-packages/requests/api.py", line 44, in request
    return session.request(method=method, url=url, **kwargs)
  File "/usr/lib/python2.7/site-packages/requests/sessions.py", line 456, in request
    resp = self.send(prep, **send_kwargs)
  File "/usr/lib/python2.7/site-packages/requests/sessions.py", line 559, in send
    r = adapter.send(request, **kwargs)
  File "/usr/lib/python2.7/site-packages/requests/adapters.py", line 382, in send
    raise SSLError(e, request=request)
requests.exceptions.SSLError: hostname '&lt;Proxy&gt;' doesn't match either of 'github.com', 'www.github.com'
</code></pre></div>
<p dir="auto">where  is the URL of the http(s)-proxy I'm using. I have set the environment-variables HTTP_PROXY and HTTPS_PROXY (lowercase, too) to that URL. Is it possible to avoid that error without appending verify=False so I don't have to mess with other projects?</p>