Repository: encode/httpx

Issue:
Add support for SSLKEYLOGFILE for Python 3.8b4+
Python 3.8b4 (with OpenSSL 1.1.1) added support for the `SSLKEYLOGFILE` environment variable which will be very useful for using Wireshark to do analysis of HTTP/2 requests for example.

This should be a small change within `SSLConfig._create_default_ssl_context()` see [this commit](https://github.com/python/cpython/commit/c7f7069e77c58e83b847c0bfe4d5aadf6add2e68) for the gist of how to implement.

Would also require that we document within the Environment Variables section that we support this environment variable and under what circumstances.

Discussion:
tomasfarias: Tests are passing with my local installation of Python 3.8.0b4, the error suggests travis-ci may not be running the same version (as the `keylog_filename` attribute was added in 3.8.0b4), but it seems to be correct given the logs:

```
$ python --version
Python 3.8.0b4+
``` 

Does anybody have any idea why?
tomasfarias: I started an interactive shell using the same Python 3.8 travis uses and got:

```
Python 3.8.0b4+ (heads/3.8:6922b9e, Aug 31 2019, 09:14:24)
```

Which is not quite what I get with my own Python 3.8 venv:

```
Python 3.8.0b4 (default, Aug 31 2019, 17:23:49)
```

So there might be a difference in versions after all.

florimondmanca: @tomasfarias Could this be related to the installed OpenSSL version on Travis? The build reads:

```console
openssl version
OpenSSL 1.0.2g  1 Mar 2016
```

But the new keylog file requires OpenSSL 1.1+, right? Perhaps the `ssl` API depends on the available OpenSSL version, which could explain the error we see in the build.
florimondmanca: I did a bit of research anticipating the need to use OpenSSL 1.1, and there doesn't seem to be an easy way to get it on Travis other than install it ourselves (or move to Docker-based testing…).

There's an [open issue](https://bugs.launchpad.net/xenial-backports/+bug/1785412) on upgrading it in Xenial though — which I assume might end up in Travis's `dist: xenial` environment if resolved?

For manual installation, I found [this commit](https://github.com/ruby/ruby/pull/1980/files) adding OpenSSL 1.1 to Ruby's TravisCI setup. I don't know yet which way we'd like to go it does happen that the OpenSSL version is causing the error here.
tomasfarias: Thanks for pointing out the issue. We could also skip the test, mimicking what's being done [here],(https://github.com/python/cpython/commit/c7f7069e77c58e83b847c0bfe4d5aadf6add2e68#diff-e144a9cacd10921d4dae0aeac0300a6fR4410) but with `pytest.mark`.
florimondmanca: > We could also skip the test, mimicking what's being done [here](https://github.com/python/cpython/commit/c7f7069e77c58e83b847c0bfe4d5aadf6add2e68#diff-e144a9cacd10921d4dae0aeac0300a6fR4410), but with `pytest.mark`.

Yes — that would definitely be useful for contributors who may not have OpenSSL 1.1 installed.

I think we do want to test this on CI, though.

Can you try adding `dist: bionic` on the 3.8 matrix job? It should make the build run on Ubuntu 18.04 (see [here](https://docs.travis-ci.com/user/reference/bionic)), which I believe comes with OpenSSL 1.1 ([source](https://launchpad.net/ubuntu/bionic/+source/openssl)). If it works, let's add a comment and celebrate. :-)

Patch:
diff --git a/.travis.yml b/.travis.yml
index 07207f9..2a8da53 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -20,6 +20,7 @@ matrix:
       env: NOX_SESSION=test-3.7
     - python: 3.8-dev
       env: NOX_SESSION=test-3.8
+      dist: bionic # Required to get OpenSSL 1.1.1+
 
 install:
   - pip install --upgrade nox
diff --git a/docs/environment_variables.md b/docs/environment_variables.md
index be1b581..d8f3960 100644
--- a/docs/environment_variables.md
+++ b/docs/environment_variables.md
@@ -44,3 +44,38 @@ user@host:~$ HTTPX_DEBUG=1 python test_script.py
 20:54:17.742 - httpx.dispatch.http2 - receive_event stream_id=0 event=<PingReceived ping_data:0000000000000000>
 20:54:17.743 - httpx.dispatch.connection_pool - release_connection connection=HTTPConnection(origin=Origin(scheme='https' host='www.google.com' port=443))
 ```
+
+`SSLKEYLOGFILE`
+-----------
+
+Valid values: a filename
+
+If this environment variable is set, TLS keys will be appended to the specified file, creating it if it doesn't exist, whenever key material is generated or received. The keylog file is designed for debugging purposes only.
+
+Support for `SSLKEYLOGFILE` requires Python 3.8 and OpenSSL 1.1.1 or newer.
+
+Example:
+
+```python
+# test_script.py
+
+import httpx
+client = httpx.Client()
+client.get("https://google.com")
+```
+
+```console
+SSLKEYLOGFILE=test.log python test_script.py
+cat test.log
+# TLS secrets log file, generated by OpenSSL / Python
+SERVER_HANDSHAKE_TRAFFIC_SECRET XXXX
+EXPORTER_SECRET XXXX
+SERVER_TRAFFIC_SECRET_0 XXXX
+CLIENT_HANDSHAKE_TRAFFIC_SECRET XXXX
+CLIENT_TRAFFIC_SECRET_0 XXXX
+SERVER_HANDSHAKE_TRAFFIC_SECRET XXXX
+EXPORTER_SECRET XXXX
+SERVER_TRAFFIC_SECRET_0 XXXX
+CLIENT_HANDSHAKE_TRAFFIC_SECRET XXXX
+CLIENT_TRAFFIC_SECRET_0 XXXX
+```
diff --git a/httpx/client.py b/httpx/client.py
index 44a0921..3c135e2 100644
--- a/httpx/client.py
+++ b/httpx/client.py
@@ -82,6 +82,8 @@ class BaseClient:
             else:
                 dispatch = ASGIDispatch(app=app)
 
+        self.trust_env = True if trust_env is None else trust_env
+
         if dispatch is None:
             async_dispatch: AsyncDispatcher = ConnectionPool(
                 verify=verify,
@@ -90,6 +92,7 @@ class BaseClient:
                 http_versions=http_versions,
                 pool_limits=pool_limits,
                 backend=backend,
+                trust_env=self.trust_env,
             )
         elif isinstance(dispatch, Dispatcher):
             async_dispatch = ThreadedDispatcher(dispatch, backend)
@@ -107,7 +110,6 @@ class BaseClient:
         self.max_redirects = max_redirects
         self.dispatch = async_dispatch
         self.concurrency_backend = backend
-        self.trust_env = True if trust_env is None else trust_env
 
     @property
     def headers(self) -> Headers:
diff --git a/httpx/config.py b/httpx/config.py
index 4082a9f..99e9240 100644
--- a/httpx/config.py
+++ b/httpx/config.py
@@ -1,3 +1,4 @@
+import os
 import ssl
 import typing
 from pathlib import Path
@@ -43,7 +44,13 @@ class SSLConfig:
     SSL Configuration.
     """
 
-    def __init__(self, *, cert: CertTypes = None, verify: VerifyTypes = True):
+    def __init__(
+        self,
+        *,
+        cert: CertTypes = None,
+        verify: VerifyTypes = True,
+        trust_env: bool = None,
+    ):
         self.cert = cert
 
         # Allow passing in our own SSLContext object that's pre-configured.
@@ -56,6 +63,7 @@ class SSLConfig:
 
         self.ssl_context: typing.Optional[ssl.SSLContext] = ssl_context
         self.verify: typing.Union[str, bool] = verify
+        self.trust_env = trust_env
 
     def __eq__(self, other: typing.Any) -> bool:
         return (
@@ -166,6 +174,11 @@ class SSLConfig:
         if ssl.HAS_NPN:  # pragma: no cover
             context.set_npn_protocols(http_versions.alpn_identifiers)
 
+        if hasattr(context, "keylog_filename"):
+            keylogfile = os.environ.get("SSLKEYLOGFILE")
+            if keylogfile and self.trust_env:
+                context.keylog_filename = keylogfile  # type: ignore
+
         return context
 
     def _load_client_certs(self, ssl_context: ssl.SSLContext) -> None:
diff --git a/httpx/dispatch/connection.py b/httpx/dispatch/connection.py
index b843c2e..529d373 100644
--- a/httpx/dispatch/connection.py
+++ b/httpx/dispatch/connection.py
@@ -33,13 +33,14 @@ class HTTPConnection(AsyncDispatcher):
         origin: typing.Union[str, Origin],
         verify: VerifyTypes = True,
         cert: CertTypes = None,
+        trust_env: bool = None,
         timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
         http_versions: HTTPVersionTypes = None,
         backend: ConcurrencyBackend = None,
         release_func: typing.Optional[ReleaseCallback] = None,
     ):
         self.origin = Origin(origin) if isinstance(origin, str) else origin
-        self.ssl = SSLConfig(cert=cert, verify=verify)
+        self.ssl = SSLConfig(cert=cert, verify=verify, trust_env=trust_env)
         self.timeout = TimeoutConfig(timeout)
         self.http_versions = HTTPVersionConfig(http_versions)
         self.backend = AsyncioBackend() if backend is None else backend
diff --git a/httpx/dispatch/connection_pool.py b/httpx/dispatch/connection_pool.py
index 3699209..befa88c 100644
--- a/httpx/dispatch/connection_pool.py
+++ b/httpx/dispatch/connection_pool.py
@@ -84,6 +84,7 @@ class ConnectionPool(AsyncDispatcher):
         *,
         verify: VerifyTypes = True,
         cert: CertTypes = None,
+        trust_env: bool = None,
         timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
         pool_limits: PoolLimits = DEFAULT_POOL_LIMITS,
         http_versions: HTTPVersionTypes = None,
@@ -95,6 +96,7 @@ class ConnectionPool(AsyncDispatcher):
         self.pool_limits = pool_limits
         self.http_versions = http_versions
         self.is_closed = False
+        self.trust_env = trust_env
 
         self.keepalive_connections = ConnectionStore()
         self.active_connections = ConnectionStore()
@@ -145,6 +147,7 @@ class ConnectionPool(AsyncDispatcher):
                 http_versions=self.http_versions,
                 backend=self.backend,
                 release_func=self.release_connection,
+                trust_env=self.trust_env,
             )
             logger.debug(f"new_connection connection={connection!r}")
         else:


Test patch:
diff --git a/tests/client/test_async_client.py b/tests/client/test_async_client.py
index bd29743..c028c1b 100644
--- a/tests/client/test_async_client.py
+++ b/tests/client/test_async_client.py
@@ -4,7 +4,7 @@ import httpx
 
 
 async def test_get(server, backend):
-    url = "http://127.0.0.1:8000/"
+    url = server.url
     async with httpx.AsyncClient(backend=backend) as client:
         response = await client.get(url)
     assert response.status_code == 200
@@ -19,7 +19,7 @@ async def test_get_no_backend(server):
     """
     Verify that the client is capable of making a simple request if not given a backend.
     """
-    url = "http://127.0.0.1:8000/"
+    url = server.url
     async with httpx.AsyncClient() as client:
         response = await client.get(url)
     assert response.status_code == 200
@@ -30,14 +30,14 @@ async def test_get_no_backend(server):
 
 
 async def test_post(server, backend):
-    url = "http://127.0.0.1:8000/"
+    url = server.url
     async with httpx.AsyncClient(backend=backend) as client:
         response = await client.post(url, data=b"Hello, world!")
     assert response.status_code == 200
 
 
 async def test_post_json(server, backend):
-    url = "http://127.0.0.1:8000/"
+    url = server.url
     async with httpx.AsyncClient(backend=backend) as client:
         response = await client.post(url, json={"text": "Hello, world!"})
     assert response.status_code == 200
@@ -45,7 +45,7 @@ async def test_post_json(server, backend):
 
 async def test_stream_response(server, backend):
     async with httpx.AsyncClient(backend=backend) as client:
-        response = await client.request("GET", "http://127.0.0.1:8000/", stream=True)
+        response = await client.request("GET", server.url, stream=True)
     assert response.status_code == 200
     body = await response.read()
     assert body == b"Hello, world!"
@@ -54,7 +54,7 @@ async def test_stream_response(server, backend):
 
 async def test_access_content_stream_response(server, backend):
     async with httpx.AsyncClient(backend=backend) as client:
-        response = await client.request("GET", "http://127.0.0.1:8000/", stream=True)
+        response = await client.request("GET", server.url, stream=True)
     assert response.status_code == 200
     with pytest.raises(httpx.ResponseNotRead):
         response.content
@@ -66,9 +66,7 @@ async def test_stream_request(server, backend):
         yield b"world!"
 
     async with httpx.AsyncClient(backend=backend) as client:
-        response = await client.request(
-            "POST", "http://127.0.0.1:8000/", data=hello_world()
-        )
+        response = await client.request("POST", server.url, data=hello_world())
     assert response.status_code == 200
 
 
@@ -76,7 +74,7 @@ async def test_raise_for_status(server, backend):
     async with httpx.AsyncClient(backend=backend) as client:
         for status_code in (200, 400, 404, 500, 505):
             response = await client.request(
-                "GET", f"http://127.0.0.1:8000/status/{status_code}"
+                "GET", server.url.copy_with(path=f"/status/{status_code}")
             )
 
             if 400 <= status_code < 600:
@@ -88,50 +86,46 @@ async def test_raise_for_status(server, backend):
 
 
 async def test_options(server, backend):
-    url = "http://127.0.0.1:8000/"
     async with httpx.AsyncClient(backend=backend) as client:
-        response = await client.options(url)
+        response = await client.options(server.url)
     assert response.status_code == 200
     assert response.text == "Hello, world!"
 
 
 async def test_head(server, backend):
-    url = "http://127.0.0.1:8000/"
     async with httpx.AsyncClient(backend=backend) as client:
-        response = await client.head(url)
+        response = await client.head(server.url)
     assert response.status_code == 200
     assert response.text == ""
 
 
 async def test_put(server, backend):
-    url = "http://127.0.0.1:8000/"
     async with httpx.AsyncClient(backend=backend) as client:
-        response = await client.put(url, data=b"Hello, world!")
+        response = await client.put(server.url, data=b"Hello, world!")
     assert response.status_code == 200
 
 
 async def test_patch(server, backend):
-    url = "http://127.0.0.1:8000/"
     async with httpx.AsyncClient(backend=backend) as client:
-        response = await client.patch(url, data=b"Hello, world!")
+        response = await client.patch(server.url, data=b"Hello, world!")
     assert response.status_code == 200
 
 
 async def test_delete(server, backend):
-    url = "http://127.0.0.1:8000/"
     async with httpx.AsyncClient(backend=backend) as client:
-        response = await client.delete(url)
+        response = await client.delete(server.url)
     assert response.status_code == 200
     assert response.text == "Hello, world!"
 
 
 async def test_100_continue(server, backend):
-    url = "http://127.0.0.1:8000/echo_body"
     headers = {"Expect": "100-continue"}
     data = b"Echo request body"
 
     async with httpx.AsyncClient(backend=backend) as client:
-        response = await client.post(url, headers=headers, data=data)
+        response = await client.post(
+            server.url.copy_with(path="/echo_body"), headers=headers, data=data
+        )
 
     assert response.status_code == 200
     assert response.content == data
diff --git a/tests/client/test_client.py b/tests/client/test_client.py
index 52d99e3..7397920 100644
--- a/tests/client/test_client.py
+++ b/tests/client/test_client.py
@@ -4,16 +4,16 @@ import httpx
 
 
 def test_get(server):
-    url = "http://127.0.0.1:8000/"
+    url = server.url
     with httpx.Client() as http:
         response = http.get(url)
     assert response.status_code == 200
-    assert response.url == httpx.URL(url)
+    assert response.url == url
     assert response.content == b"Hello, world!"
     assert response.text == "Hello, world!"
     assert response.http_version == "HTTP/1.1"
     assert response.encoding == "iso-8859-1"
-    assert response.request.url == httpx.URL(url)
+    assert response.request.url == url
     assert response.headers
     assert response.is_redirect is False
     assert repr(response) == "<Response [200 OK]>"
@@ -21,21 +21,21 @@ def test_get(server):
 
 def test_post(server):
     with httpx.Client() as http:
-        response = http.post("http://127.0.0.1:8000/", data=b"Hello, world!")
+        response = http.post(server.url, data=b"Hello, world!")
     assert response.status_code == 200
     assert response.reason_phrase == "OK"
 
 
 def test_post_json(server):
     with httpx.Client() as http:
-        response = http.post("http://127.0.0.1:8000/", json={"text": "Hello, world!"})
+        response = http.post(server.url, json={"text": "Hello, world!"})
     assert response.status_code == 200
     assert response.reason_phrase == "OK"
 
 
 def test_stream_response(server):
     with httpx.Client() as http:
-        response = http.get("http://127.0.0.1:8000/", stream=True)
+        response = http.get(server.url, stream=True)
     assert response.status_code == 200
     content = response.read()
     assert content == b"Hello, world!"
@@ -43,7 +43,7 @@ def test_stream_response(server):
 
 def test_stream_iterator(server):
     with httpx.Client() as http:
-        response = http.get("http://127.0.0.1:8000/", stream=True)
+        response = http.get(server.url, stream=True)
     assert response.status_code == 200
     body = b""
     for chunk in response.stream():
@@ -53,7 +53,7 @@ def test_stream_iterator(server):
 
 def test_raw_iterator(server):
     with httpx.Client() as http:
-        response = http.get("http://127.0.0.1:8000/", stream=True)
+        response = http.get(server.url, stream=True)
     assert response.status_code == 200
     body = b""
     for chunk in response.raw():
@@ -66,7 +66,7 @@ def test_raise_for_status(server):
     with httpx.Client() as client:
         for status_code in (200, 400, 404, 500, 505):
             response = client.request(
-                "GET", "http://127.0.0.1:8000/status/{}".format(status_code)
+                "GET", server.url.copy_with(path="/status/{}".format(status_code))
             )
             if 400 <= status_code < 600:
                 with pytest.raises(httpx.exceptions.HTTPError) as exc_info:
@@ -78,45 +78,45 @@ def test_raise_for_status(server):
 
 def test_options(server):
     with httpx.Client() as http:
-        response = http.options("http://127.0.0.1:8000/")
+        response = http.options(server.url)
     assert response.status_code == 200
     assert response.reason_phrase == "OK"
 
 
 def test_head(server):
     with httpx.Client() as http:
-        response = http.head("http://127.0.0.1:8000/")
+        response = http.head(server.url)
     assert response.status_code == 200
     assert response.reason_phrase == "OK"
 
 
 def test_put(server):
     with httpx.Client() as http:
-        response = http.put("http://127.0.0.1:8000/", data=b"Hello, world!")
+        response = http.put(server.url, data=b"Hello, world!")
     assert response.status_code == 200
     assert response.reason_phrase == "OK"
 
 
 def test_patch(server):
     with httpx.Client() as http:
-        response = http.patch("http://127.0.0.1:8000/", data=b"Hello, world!")
+        response = http.patch(server.url, data=b"Hello, world!")
     assert response.status_code == 200
     assert response.reason_phrase == "OK"
 
 
 def test_delete(server):
     with httpx.Client() as http:
-        response = http.delete("http://127.0.0.1:8000/")
+        response = http.delete(server.url)
     assert response.status_code == 200
     assert response.reason_phrase == "OK"
 
 
 def test_base_url(server):
-    base_url = "http://127.0.0.1:8000/"
+    base_url = server.url
     with httpx.Client(base_url=base_url) as http:
         response = http.get("/")
     assert response.status_code == 200
-    assert str(response.url) == base_url
+    assert response.url == base_url
 
 
 def test_merge_url():
diff --git a/tests/conftest.py b/tests/conftest.py
index ec75e60..86a9934 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -12,7 +12,7 @@ from cryptography.hazmat.primitives.serialization import (
 from uvicorn.config import Config
 from uvicorn.main import Server
 
-from httpx import AsyncioBackend
+from httpx import URL, AsyncioBackend
 
 
 @pytest.fixture(params=[pytest.param(AsyncioBackend, marks=pytest.mark.asyncio)])
@@ -132,6 +132,11 @@ def cert_encrypted_private_key_file(example_cert):
 
 
 class TestServer(Server):
+    @property
+    def url(self) -> URL:
+        protocol = "https" if self.config.is_ssl else "http"
+        return URL(f"{protocol}://{self.config.host}:{self.config.port}/")
+
     def install_signal_handlers(self) -> None:
         # Disable the default installation of handlers for signals such as SIGTERM,
         # because it can only be done in the main thread.
@@ -145,7 +150,6 @@ class TestServer(Server):
             loop.create_task(super().serve(sockets=sockets)),
             loop.create_task(self.watch_restarts()),
         }
-
         await asyncio.wait(tasks)
 
     async def restart(self) -> None:
diff --git a/tests/dispatch/test_connection_pools.py b/tests/dispatch/test_connection_pools.py
index 8f0ba70..2765808 100644
--- a/tests/dispatch/test_connection_pools.py
+++ b/tests/dispatch/test_connection_pools.py
@@ -6,12 +6,12 @@ async def test_keepalive_connections(server, backend):
     Connections should default to staying in a keep-alive state.
     """
     async with httpx.ConnectionPool(backend=backend) as http:
-        response = await http.request("GET", "http://127.0.0.1:8000/")
+        response = await http.request("GET", server.url)
         await response.read()
         assert len(http.active_connections) == 0
         assert len(http.keepalive_connections) == 1
 
-        response = await http.request("GET", "http://127.0.0.1:8000/")
+        response = await http.request("GET", server.url)
         await response.read()
         assert len(http.active_connections) == 0
         assert len(http.keepalive_connections) == 1
@@ -22,7 +22,7 @@ async def test_differing_connection_keys(server, backend):
     Connections to differing connection keys should result in multiple connections.
     """
     async with httpx.ConnectionPool(backend=backend) as http:
-        response = await http.request("GET", "http://127.0.0.1:8000/")
+        response = await http.request("GET", server.url)
         await response.read()
         assert len(http.active_connections) == 0
         assert len(http.keepalive_connections) == 1
@@ -40,7 +40,7 @@ async def test_soft_limit(server, backend):
     pool_limits = httpx.PoolLimits(soft_limit=1)
 
     async with httpx.ConnectionPool(pool_limits=pool_limits, backend=backend) as http:
-        response = await http.request("GET", "http://127.0.0.1:8000/")
+        response = await http.request("GET", server.url)
         await response.read()
         assert len(http.active_connections) == 0
         assert len(http.keepalive_connections) == 1
@@ -56,7 +56,7 @@ async def test_streaming_response_holds_connection(server, backend):
     A streaming request should hold the connection open until the response is read.
     """
     async with httpx.ConnectionPool(backend=backend) as http:
-        response = await http.request("GET", "http://127.0.0.1:8000/")
+        response = await http.request("GET", server.url)
         assert len(http.active_connections) == 1
         assert len(http.keepalive_connections) == 0
 
@@ -71,11 +71,11 @@ async def test_multiple_concurrent_connections(server, backend):
     Multiple conncurrent requests should open multiple conncurrent connections.
     """
     async with httpx.ConnectionPool(backend=backend) as http:
-        response_a = await http.request("GET", "http://127.0.0.1:8000/")
+        response_a = await http.request("GET", server.url)
         assert len(http.active_connections) == 1
         assert len(http.keepalive_connections) == 0
 
-        response_b = await http.request("GET", "http://127.0.0.1:8000/")
+        response_b = await http.request("GET", server.url)
         assert len(http.active_connections) == 2
         assert len(http.keepalive_connections) == 0
 
@@ -94,7 +94,7 @@ async def test_close_connections(server, backend):
     """
     headers = [(b"connection", b"close")]
     async with httpx.ConnectionPool(backend=backend) as http:
-        response = await http.request("GET", "http://127.0.0.1:8000/", headers=headers)
+        response = await http.request("GET", server.url, headers=headers)
         await response.read()
         assert len(http.active_connections) == 0
         assert len(http.keepalive_connections) == 0
@@ -105,7 +105,7 @@ async def test_standard_response_close(server, backend):
     A standard close should keep the connection open.
     """
     async with httpx.ConnectionPool(backend=backend) as http:
-        response = await http.request("GET", "http://127.0.0.1:8000/")
+        response = await http.request("GET", server.url)
         await response.read()
         await response.close()
         assert len(http.active_connections) == 0
@@ -117,7 +117,7 @@ async def test_premature_response_close(server, backend):
     A premature close should close the connection.
     """
     async with httpx.ConnectionPool(backend=backend) as http:
-        response = await http.request("GET", "http://127.0.0.1:8000/")
+        response = await http.request("GET", server.url)
         await response.close()
         assert len(http.active_connections) == 0
         assert len(http.keepalive_connections) == 0
@@ -131,13 +131,13 @@ async def test_keepalive_connection_closed_by_server_is_reestablished(
     should be reestablished.
     """
     async with httpx.ConnectionPool(backend=backend) as http:
-        response = await http.request("GET", "http://127.0.0.1:8000/")
+        response = await http.request("GET", server.url)
         await response.read()
 
         # Shutdown the server to close the keep-alive connection
         await restart(server)
 
-        response = await http.request("GET", "http://127.0.0.1:8000/")
+        response = await http.request("GET", server.url)
         await response.read()
         assert len(http.active_connections) == 0
         assert len(http.keepalive_connections) == 1
@@ -151,13 +151,13 @@ async def test_keepalive_http2_connection_closed_by_server_is_reestablished(
     should be reestablished.
     """
     async with httpx.ConnectionPool(backend=backend) as http:
-        response = await http.request("GET", "http://127.0.0.1:8000/")
+        response = await http.request("GET", server.url)
         await response.read()
 
         # Shutdown the server to close the keep-alive connection
         await restart(server)
 
-        response = await http.request("GET", "http://127.0.0.1:8000/")
+        response = await http.request("GET", server.url)
         await response.read()
         assert len(http.active_connections) == 0
         assert len(http.keepalive_connections) == 1
@@ -169,11 +169,11 @@ async def test_connection_closed_free_semaphore_on_acquire(server, restart, back
     properly on a disconnected connection.
     """
     async with httpx.ConnectionPool(pool_limits=httpx.PoolLimits(hard_limit=1)) as http:
-        response = await http.request("GET", "http://127.0.0.1:8000/")
+        response = await http.request("GET", server.url)
         await response.read()
 
         # Close the connection so we're forced to recycle it
         await restart(server)
 
-        response = await http.request("GET", "http://127.0.0.1:8000/")
+        response = await http.request("GET", server.url)
         assert response.status_code == 200
diff --git a/tests/dispatch/test_connections.py b/tests/dispatch/test_connections.py
index 0aa7741..14386aa 100644
--- a/tests/dispatch/test_connections.py
+++ b/tests/dispatch/test_connections.py
@@ -2,18 +2,16 @@ from httpx import HTTPConnection
 
 
 async def test_get(server, backend):
-    async with HTTPConnection(origin="http://127.0.0.1:8000/", backend=backend) as conn:
-        response = await conn.request("GET", "http://127.0.0.1:8000/")
+    async with HTTPConnection(origin=server.url, backend=backend) as conn:
+        response = await conn.request("GET", server.url)
         await response.read()
         assert response.status_code == 200
         assert response.content == b"Hello, world!"
 
 
 async def test_post(server, backend):
-    async with HTTPConnection(origin="http://127.0.0.1:8000/", backend=backend) as conn:
-        response = await conn.request(
-            "GET", "http://127.0.0.1:8000/", data=b"Hello, world!"
-        )
+    async with HTTPConnection(origin=server.url, backend=backend) as conn:
+        response = await conn.request("GET", server.url, data=b"Hello, world!")
         assert response.status_code == 200
 
 
@@ -22,9 +20,9 @@ async def test_https_get_with_ssl_defaults(https_server, backend):
     An HTTPS request, with default SSL configuration set on the client.
     """
     async with HTTPConnection(
-        origin="https://127.0.0.1:8001/", verify=False, backend=backend
+        origin=https_server.url, verify=False, backend=backend
     ) as conn:
-        response = await conn.request("GET", "https://127.0.0.1:8001/")
+        response = await conn.request("GET", https_server.url)
         await response.read()
         assert response.status_code == 200
         assert response.content == b"Hello, world!"
@@ -34,10 +32,8 @@ async def test_https_get_with_sll_overrides(https_server, backend):
     """
     An HTTPS request, with SSL configuration set on the request.
     """
-    async with HTTPConnection(
-        origin="https://127.0.0.1:8001/", backend=backend
-    ) as conn:
-        response = await conn.request("GET", "https://127.0.0.1:8001/", verify=False)
+    async with HTTPConnection(origin=https_server.url, backend=backend) as conn:
+        response = await conn.request("GET", https_server.url, verify=False)
         await response.read()
         assert response.status_code == 200
         assert response.content == b"Hello, world!"
diff --git a/tests/test_api.py b/tests/test_api.py
index cb9eb20..1500869 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -4,7 +4,7 @@ import httpx
 
 
 def test_get(server):
-    response = httpx.get("http://127.0.0.1:8000/")
+    response = httpx.get(server.url)
     assert response.status_code == 200
     assert response.reason_phrase == "OK"
     assert response.text == "Hello, world!"
@@ -12,7 +12,7 @@ def test_get(server):
 
 
 def test_post(server):
-    response = httpx.post("http://127.0.0.1:8000/", data=b"Hello, world!")
+    response = httpx.post(server.url, data=b"Hello, world!")
     assert response.status_code == 200
     assert response.reason_phrase == "OK"
 
@@ -23,37 +23,37 @@ def test_post_byte_iterator(server):
         yield b", "
         yield b"world!"
 
-    response = httpx.post("http://127.0.0.1:8000/", data=data())
+    response = httpx.post(server.url, data=data())
     assert response.status_code == 200
     assert response.reason_phrase == "OK"
 
 
 def test_options(server):
-    response = httpx.options("http://127.0.0.1:8000/")
+    response = httpx.options(server.url)
     assert response.status_code == 200
     assert response.reason_phrase == "OK"
 
 
 def test_head(server):
-    response = httpx.head("http://127.0.0.1:8000/")
+    response = httpx.head(server.url)
     assert response.status_code == 200
     assert response.reason_phrase == "OK"
 
 
 def test_put(server):
-    response = httpx.put("http://127.0.0.1:8000/", data=b"Hello, world!")
+    response = httpx.put(server.url, data=b"Hello, world!")
     assert response.status_code == 200
     assert response.reason_phrase == "OK"
 
 
 def test_patch(server):
-    response = httpx.patch("http://127.0.0.1:8000/", data=b"Hello, world!")
+    response = httpx.patch(server.url, data=b"Hello, world!")
     assert response.status_code == 200
     assert response.reason_phrase == "OK"
 
 
 def test_delete(server):
-    response = httpx.delete("http://127.0.0.1:8000/")
+    response = httpx.delete(server.url)
     assert response.status_code == 200
     assert response.reason_phrase == "OK"
 
diff --git a/tests/test_concurrency.py b/tests/test_concurrency.py
index 2fa0161..b9312ea 100644
--- a/tests/test_concurrency.py
+++ b/tests/test_concurrency.py
@@ -19,13 +19,15 @@ async def test_start_tls_on_socket_stream(https_server):
     ctx = SSLConfig().load_ssl_context_no_verify(HTTPVersionConfig())
     timeout = TimeoutConfig(5)
 
-    stream = await backend.connect("127.0.0.1", 8001, None, timeout)
+    stream = await backend.connect(
+        https_server.url.host, https_server.url.port, None, timeout
+    )
 
     try:
         assert stream.is_connection_dropped() is False
         assert stream.stream_writer.get_extra_info("cipher", default=None) is None
 
-        stream = await backend.start_tls(stream, "127.0.0.1", ctx, timeout)
+        stream = await backend.start_tls(stream, https_server.url.host, ctx, timeout)
         assert stream.is_connection_dropped() is False
         assert stream.stream_writer.get_extra_info("cipher", default=None) is not None
 
diff --git a/tests/test_config.py b/tests/test_config.py
index 0df1029..fefc06d 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -1,4 +1,5 @@
 import ssl
+import sys
 
 import pytest
 
@@ -171,3 +172,33 @@ def test_timeout_from_tuple():
 def test_timeout_from_config_instance():
     timeout = httpx.TimeoutConfig(timeout=5.0)
     assert httpx.TimeoutConfig(timeout) == httpx.TimeoutConfig(timeout=5.0)
+
+
+@pytest.mark.skipif(
+    not hasattr(ssl.SSLContext, "keylog_filename"),
+    reason="requires OpenSSL 1.1.1 or higher",
+)
+@pytest.mark.skipif(sys.version_info < (3, 8), reason="requires python3.8 or higher")
+def test_ssl_config_support_for_keylog_file(tmpdir, monkeypatch):
+    with monkeypatch.context() as m:
+        m.delenv("SSLKEYLOGFILE", raising=False)
+
+        ssl_config = httpx.SSLConfig(trust_env=True)
+        ssl_config.load_ssl_context()
+
+        assert ssl_config.ssl_context.keylog_filename is None
+
+    filename = str(tmpdir.join("test.log"))
+
+    with monkeypatch.context() as m:
+        m.setenv("SSLKEYLOGFILE", filename)
+
+        ssl_config = httpx.SSLConfig(trust_env=True)
+        ssl_config.load_ssl_context()
+
+        assert ssl_config.ssl_context.keylog_filename == filename
+
+        ssl_config = httpx.SSLConfig(trust_env=False)
+        ssl_config.load_ssl_context()
+
+        assert ssl_config.ssl_context.keylog_filename is None
diff --git a/tests/test_timeouts.py b/tests/test_timeouts.py
index 540fa91..765c8f7 100644
--- a/tests/test_timeouts.py
+++ b/tests/test_timeouts.py
@@ -16,7 +16,7 @@ async def test_read_timeout(server, backend):
 
     async with AsyncClient(timeout=timeout, backend=backend) as client:
         with pytest.raises(ReadTimeout):
-            await client.get("http://127.0.0.1:8000/slow_response")
+            await client.get(server.url.copy_with(path="/slow_response"))
 
 
 async def test_write_timeout(server, backend):
@@ -25,7 +25,7 @@ async def test_write_timeout(server, backend):
     async with AsyncClient(timeout=timeout, backend=backend) as client:
         with pytest.raises(WriteTimeout):
             data = b"*" * 1024 * 1024 * 100
-            await client.put("http://127.0.0.1:8000/slow_response", data=data)
+            await client.put(server.url.copy_with(path="/slow_response"), data=data)
 
 
 async def test_connect_timeout(server, backend):
@@ -41,7 +41,7 @@ async def test_pool_timeout(server, backend):
     pool_limits = PoolLimits(hard_limit=1, pool_timeout=1e-6)
 
     async with AsyncClient(pool_limits=pool_limits, backend=backend) as client:
-        response = await client.get("http://127.0.0.1:8000/", stream=True)
+        response = await client.get(server.url, stream=True)
 
         with pytest.raises(PoolTimeout):
             await client.get("http://localhost:8000/")
diff --git a/tests/test_utils.py b/tests/test_utils.py
index bb2abb4..d3dad73 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -102,7 +102,7 @@ async def test_httpx_debug_enabled_stderr_logging(server, capsys, httpx_debug):
     utils.get_logger("httpx")
 
     async with httpx.AsyncClient() as client:
-        await client.get("http://127.0.0.1:8000/")
+        await client.get(server.url)
 
     if httpx_debug in ("1", "True"):
         assert "httpx.dispatch.connection_pool" in capsys.readouterr().err