tomchristie: Missing a final commit that fixes `.is_absolute` to `.is_absolute_url`.

I'm assuming GitHub's just lagging here, since it appears pushed to me.
diff --git a/httpx/__version__.py b/httpx/__version__.py
index c5079f7..b2e30d9 100644
--- a/httpx/__version__.py
+++ b/httpx/__version__.py
@@ -1,3 +1,3 @@
 __title__ = "httpx"
 __description__ = "A next generation HTTP client, for Python 3."
-__version__ = "0.6.7"
+__version__ = "0.6.8"
diff --git a/httpx/concurrency.py b/httpx/concurrency.py
index b8246b5..2ee45a8 100644
--- a/httpx/concurrency.py
+++ b/httpx/concurrency.py
@@ -108,6 +108,9 @@ class Reader(BaseReader):
 
         return data
 
+    def is_connection_dropped(self) -> bool:
+        return self.stream_reader.at_eof()
+
 
 class Writer(BaseWriter):
     def __init__(self, stream_writer: asyncio.StreamWriter, timeout: TimeoutConfig):
diff --git a/httpx/dispatch/connection.py b/httpx/dispatch/connection.py
index b1400af..4a303f2 100644
--- a/httpx/dispatch/connection.py
+++ b/httpx/dispatch/connection.py
@@ -102,3 +102,10 @@ class HTTPConnection(AsyncDispatcher):
         else:
             assert self.h11_connection is not None
             return self.h11_connection.is_closed
+
+    def is_connection_dropped(self) -> bool:
+        if self.h2_connection is not None:
+            return self.h2_connection.is_connection_dropped()
+        else:
+            assert self.h11_connection is not None
+            return self.h11_connection.is_connection_dropped()
diff --git a/httpx/dispatch/connection_pool.py b/httpx/dispatch/connection_pool.py
index 0b827c1..b6c0457 100644
--- a/httpx/dispatch/connection_pool.py
+++ b/httpx/dispatch/connection_pool.py
@@ -9,7 +9,6 @@ from ..config import (
     TimeoutTypes,
     VerifyTypes,
 )
-from ..exceptions import NotConnected
 from ..interfaces import AsyncDispatcher, ConcurrencyBackend
 from ..models import AsyncRequest, AsyncResponse, Origin
 from .connection import HTTPConnection
@@ -108,35 +107,25 @@ class ConnectionPool(AsyncDispatcher):
         cert: CertTypes = None,
         timeout: TimeoutTypes = None,
     ) -> AsyncResponse:
-        allow_connection_reuse = True
-        connection = None
-        while connection is None:
-            connection = await self.acquire_connection(
-                origin=request.url.origin, allow_connection_reuse=allow_connection_reuse
+        connection = await self.acquire_connection(origin=request.url.origin)
+        try:
+            response = await connection.send(
+                request, verify=verify, cert=cert, timeout=timeout
             )
-            try:
-                response = await connection.send(
-                    request, verify=verify, cert=cert, timeout=timeout
-                )
-            except BaseException as exc:
-                self.active_connections.remove(connection)
-                self.max_connections.release()
-                if isinstance(exc, NotConnected) and allow_connection_reuse:
-                    connection = None
-                    allow_connection_reuse = False
-                else:
-                    raise exc
+        except BaseException as exc:
+            self.active_connections.remove(connection)
+            self.max_connections.release()
+            raise exc
 
         return response
 
-    async def acquire_connection(
-        self, origin: Origin, allow_connection_reuse: bool = True
-    ) -> HTTPConnection:
-        connection = None
-        if allow_connection_reuse:
-            connection = self.active_connections.pop_by_origin(origin, http2_only=True)
-            if connection is None:
-                connection = self.keepalive_connections.pop_by_origin(origin)
+    async def acquire_connection(self, origin: Origin) -> HTTPConnection:
+        connection = self.active_connections.pop_by_origin(origin, http2_only=True)
+        if connection is None:
+            connection = self.keepalive_connections.pop_by_origin(origin)
+
+        if connection is not None and connection.is_connection_dropped():
+            connection = None
 
         if connection is None:
             await self.max_connections.acquire()
diff --git a/httpx/dispatch/http11.py b/httpx/dispatch/http11.py
index c239535..dd0e24a 100644
--- a/httpx/dispatch/http11.py
+++ b/httpx/dispatch/http11.py
@@ -4,7 +4,6 @@ import h11
 
 from ..concurrency import TimeoutFlag
 from ..config import TimeoutConfig, TimeoutTypes
-from ..exceptions import NotConnected
 from ..interfaces import BaseReader, BaseWriter, ConcurrencyBackend
 from ..models import AsyncRequest, AsyncResponse
 
@@ -46,12 +45,7 @@ class HTTP11Connection:
     ) -> AsyncResponse:
         timeout = None if timeout is None else TimeoutConfig(timeout)
 
-        try:
-            await self._send_request(request, timeout)
-        except ConnectionResetError:  # pragma: nocover
-            # We're currently testing this case in HTTP/2.
-            # Really we should test it here too, but this'll do in the meantime.
-            raise NotConnected() from None
+        await self._send_request(request, timeout)
 
         task, args = self._send_request_data, [request.stream(), timeout]
         async with self.backend.background_manager(task, args=args):
@@ -188,3 +182,6 @@ class HTTP11Connection:
     @property
     def is_closed(self) -> bool:
         return self.h11_state.our_state in (h11.CLOSED, h11.ERROR)
+
+    def is_connection_dropped(self) -> bool:
+        return self.reader.is_connection_dropped()
diff --git a/httpx/dispatch/http2.py b/httpx/dispatch/http2.py
index 35d487a..331f82d 100644
--- a/httpx/dispatch/http2.py
+++ b/httpx/dispatch/http2.py
@@ -6,7 +6,6 @@ import h2.events
 
 from ..concurrency import TimeoutFlag
 from ..config import TimeoutConfig, TimeoutTypes
-from ..exceptions import NotConnected
 from ..interfaces import BaseReader, BaseWriter, ConcurrencyBackend
 from ..models import AsyncRequest, AsyncResponse
 
@@ -39,10 +38,7 @@ class HTTP2Connection:
         if not self.initialized:
             self.initiate_connection()
 
-        try:
-            stream_id = await self.send_headers(request, timeout)
-        except ConnectionResetError:
-            raise NotConnected() from None
+        stream_id = await self.send_headers(request, timeout)
 
         self.events[stream_id] = []
         self.timeout_flags[stream_id] = TimeoutFlag()
@@ -176,3 +172,6 @@ class HTTP2Connection:
     @property
     def is_closed(self) -> bool:
         return False
+
+    def is_connection_dropped(self) -> bool:
+        return self.reader.is_connection_dropped()
diff --git a/httpx/exceptions.py b/httpx/exceptions.py
index 3305e2d..19af3e6 100644
--- a/httpx/exceptions.py
+++ b/httpx/exceptions.py
@@ -34,13 +34,6 @@ class PoolTimeout(Timeout):
 # HTTP exceptions...
 
 
-class NotConnected(Exception):
-    """
-    A connection was lost at the point of starting a request,
-    prior to any writes succeeding.
-    """
-
-
 class HttpError(Exception):
     """
     An HTTP error occurred.
diff --git a/httpx/interfaces.py b/httpx/interfaces.py
index 02d11ce..f058ede 100644
--- a/httpx/interfaces.py
+++ b/httpx/interfaces.py
@@ -130,6 +130,9 @@ class BaseReader:
     ) -> bytes:
         raise NotImplementedError()  # pragma: no cover
 
+    def is_connection_dropped(self) -> bool:
+        raise NotImplementedError()  # pragma: no cover
+
 
 class BaseWriter:
     """
diff --git a/httpx/models.py b/httpx/models.py
index 9cbdc25..04754af 100644
--- a/httpx/models.py
+++ b/httpx/models.py
@@ -98,7 +98,8 @@ class URL:
                 self.components = self.components.copy_with(authority=idna_authority)
 
         # Normalize scheme and domain name.
-        self.components = self.components.normalize()
+        if self.is_absolute_url:
+            self.components = self.components.normalize()
 
         # Add any query parameters.
         if params:
@@ -170,7 +171,7 @@ class URL:
         Return `True` for absolute URLs such as 'http://example.com/path',
         and `False` for relative URLs such as '/path'.
         """
-        # We don't use rfc3986's `is_absolute` because it treats
+        # We don't use `.is_absolute` from `rfc3986` because it treats
         # URLs with a fragment portion as not absolute.
         # What we actually care about is if the URL provides
         # a scheme and hostname to which connections should be made.

URL.join behaviour is incorrect.
Our `URL.join` behavior currently isn't as expected. It should be in line with rfc3986#section-5.2, and Python's existing `urljoin`.

It ain't...

```python
>>> httpx.URL('http://www.example.com/some/directory/abc.html').join('../../foo.html')
URL('http://www.example.com/some/directory/foo.html')
```
diff --git a/tests/dispatch/test_connection_pools.py b/tests/dispatch/test_connection_pools.py
index 5ef884e..14fd40c 100644
--- a/tests/dispatch/test_connection_pools.py
+++ b/tests/dispatch/test_connection_pools.py
@@ -131,3 +131,39 @@ async def test_premature_response_close(server):
         await response.close()
         assert len(http.active_connections) == 0
         assert len(http.keepalive_connections) == 0
+
+
+@pytest.mark.asyncio
+async def test_keepalive_connection_closed_by_server_is_reestablished(server):
+    """
+    Upon keep-alive connection closed by remote a new connection should be reestablished.
+    """
+    async with httpx.ConnectionPool() as http:
+        response = await http.request("GET", "http://127.0.0.1:8000/")
+        await response.read()
+
+        await server.shutdown()  # shutdown the server to close the keep-alive connection
+        await server.startup()
+
+        response = await http.request("GET", "http://127.0.0.1:8000/")
+        await response.read()
+        assert len(http.active_connections) == 0
+        assert len(http.keepalive_connections) == 1
+
+
+@pytest.mark.asyncio
+async def test_keepalive_http2_connection_closed_by_server_is_reestablished(server):
+    """
+    Upon keep-alive connection closed by remote a new connection should be reestablished.
+    """
+    async with httpx.ConnectionPool() as http:
+        response = await http.request("GET", "http://127.0.0.1:8000/")
+        await response.read()
+
+        await server.shutdown()  # shutdown the server to close the keep-alive connection
+        await server.startup()
+
+        response = await http.request("GET", "http://127.0.0.1:8000/")
+        await response.read()
+        assert len(http.active_connections) == 0
+        assert len(http.keepalive_connections) == 1
diff --git a/tests/dispatch/test_http2.py b/tests/dispatch/test_http2.py
index b194524..99caab6 100644
--- a/tests/dispatch/test_http2.py
+++ b/tests/dispatch/test_http2.py
@@ -68,7 +68,7 @@ def test_http2_reconnect():
 
     with Client(backend=backend) as client:
         response_1 = client.get("http://example.org/1")
-        backend.server.raise_disconnect = True
+        backend.server.close_connection = True
         response_2 = client.get("http://example.org/2")
 
     assert response_1.status_code == 200
diff --git a/tests/dispatch/utils.py b/tests/dispatch/utils.py
index a1e02a4..8a62554 100644
--- a/tests/dispatch/utils.py
+++ b/tests/dispatch/utils.py
@@ -43,7 +43,7 @@ class MockHTTP2Server(BaseReader, BaseWriter):
         self.app = app
         self.buffer = b""
         self.requests = {}
-        self.raise_disconnect = False
+        self.close_connection = False
 
     # BaseReader interface
 
@@ -55,9 +55,6 @@ class MockHTTP2Server(BaseReader, BaseWriter):
     # BaseWriter interface
 
     def write_no_block(self, data: bytes) -> None:
-        if self.raise_disconnect:
-            self.raise_disconnect = False
-            raise ConnectionResetError()
         events = self.conn.receive_data(data)
         self.buffer += self.conn.data_to_send()
         for event in events:
@@ -74,6 +71,9 @@ class MockHTTP2Server(BaseReader, BaseWriter):
     async def close(self) -> None:
         pass
 
+    def is_connection_dropped(self) -> bool:
+        return self.close_connection
+
     # Server implementation
 
     def request_received(self, headers, stream_id):
diff --git a/tests/models/test_url.py b/tests/models/test_url.py
index 70089e0..01489ac 100644
--- a/tests/models/test_url.py
+++ b/tests/models/test_url.py
@@ -1,4 +1,6 @@
 from httpx import URL
+from httpx.exceptions import InvalidURL
+import pytest
 
 
 def test_idna_url():
@@ -31,7 +33,7 @@ def test_url_eq_str():
     assert str(url) == url
 
 
-def test_url__params():
+def test_url_params():
     url = URL("https://example.org:123/path/to/somewhere", params={"a": "123"})
     assert str(url) == "https://example.org:123/path/to/somewhere?a=123"
 
@@ -39,6 +41,75 @@ def test_url__params():
     assert str(url) == "https://example.org:123/path/to/somewhere?a=123"
 
 
+def test_url_join():
+    """
+    Some basic URL joining tests.
+    """
+    url = URL("https://example.org:123/path/to/somewhere")
+    assert url.join('/somewhere-else') == "https://example.org:123/somewhere-else"
+    assert url.join('somewhere-else') == "https://example.org:123/path/to/somewhere-else"
+    assert url.join('../somewhere-else') == "https://example.org:123/path/somewhere-else"
+    assert url.join('../../somewhere-else') == "https://example.org:123/somewhere-else"
+
+
+def test_url_join_rfc3986():
+    """
+    URL joining tests, as-per reference examples in RFC 3986.
+
+    https://tools.ietf.org/html/rfc3986#section-5.4
+    """
+
+    url = URL("http://example.com/b/c/d;p?q")
+
+    with pytest.raises(InvalidURL):
+        assert url.join("g:h") == "g:h"
+
+    assert url.join("g") == "http://example.com/b/c/g"
+    assert url.join("./g") == "http://example.com/b/c/g"
+    assert url.join("g/") == "http://example.com/b/c/g/"
+    assert url.join("/g") == "http://example.com/g"
+    assert url.join("//g") == "http://g"
+    assert url.join("?y") == "http://example.com/b/c/d;p?y"
+    assert url.join("g?y") == "http://example.com/b/c/g?y"
+    assert url.join("#s") == "http://example.com/b/c/d;p?q#s"
+    assert url.join("g#s") == "http://example.com/b/c/g#s"
+    assert url.join("g?y#s") == "http://example.com/b/c/g?y#s"
+    assert url.join(";x") == "http://example.com/b/c/;x"
+    assert url.join("g;x") == "http://example.com/b/c/g;x"
+    assert url.join("g;x?y#s") == "http://example.com/b/c/g;x?y#s"
+    assert url.join("") == "http://example.com/b/c/d;p?q"
+    assert url.join(".") == "http://example.com/b/c/"
+    assert url.join("./") == "http://example.com/b/c/"
+    assert url.join("..") == "http://example.com/b/"
+    assert url.join("../") == "http://example.com/b/"
+    assert url.join("../g") == "http://example.com/b/g"
+    assert url.join("../..") == "http://example.com/"
+    assert url.join("../../") == "http://example.com/"
+    assert url.join("../../g") == "http://example.com/g"
+
+    assert url.join("../../../g") == "http://example.com/g"
+    assert url.join("../../../../g") == "http://example.com/g"
+
+    assert url.join("/./g") == "http://example.com/g"
+    assert url.join("/../g") == "http://example.com/g"
+    assert url.join("g.") == "http://example.com/b/c/g."
+    assert url.join(".g") == "http://example.com/b/c/.g"
+    assert url.join("g..") == "http://example.com/b/c/g.."
+    assert url.join("..g") == "http://example.com/b/c/..g"
+
+    assert url.join("./../g") == "http://example.com/b/g"
+    assert url.join("./g/.") == "http://example.com/b/c/g/"
+    assert url.join("g/./h") == "http://example.com/b/c/g/h"
+    assert url.join("g/../h") == "http://example.com/b/c/h"
+    assert url.join("g;x=1/./y") == "http://example.com/b/c/g;x=1/y"
+    assert url.join("g;x=1/../y") == "http://example.com/b/c/y"
+
+    assert url.join("g?y/./x") == "http://example.com/b/c/g?y/./x"
+    assert url.join("g?y/../x") == "http://example.com/b/c/g?y/../x"
+    assert url.join("g#s/./x") == "http://example.com/b/c/g#s/./x"
+    assert url.join("g#s/../x") == "http://example.com/b/c/g#s/../x"
+
+
 def test_url_set():
     urls = (
         URL("http://example.org:123/path/to/somewhere"),
diff --git a/tests/test_multipart.py b/tests/test_multipart.py
index 9a8f12c..354bcce 100644
--- a/tests/test_multipart.py
+++ b/tests/test_multipart.py
@@ -10,11 +10,11 @@ from httpx import (
     CertTypes,
     Client,
     Dispatcher,
-    multipart,
     Request,
     Response,
     TimeoutTypes,
     VerifyTypes,
+    multipart,
 )
 
 

pip install -e .
parse_log_pytest
requirements.txt
pytest pytest-asyncio pytest-cov
apt-get update apt-get install -y gcc
3.9
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
autoflake==2.3.1
babel==2.17.0
backrefs==5.8
black==25.1.0
brotlipy==0.7.0
certifi==2025.1.31
cffi==1.17.1
chardet==3.0.4
charset-normalizer==3.4.1
click==8.1.8
colorama==0.4.6
coverage==7.8.0
cryptography==44.0.2
exceptiongroup==1.2.2
flake8==7.2.0
ghp-import==2.1.0
h11==0.8.1
h2==3.2.0
hpack==3.0.0
-e git+https://github.com/encode/httpx.git@8db36ed6a537fb2efaf6e5da07102ec0065c498c#egg=httpx
hyperframe==5.2.0
idna==2.10
importlib_metadata==8.6.1
iniconfig==2.1.0
isort==6.0.1
Jinja2==3.1.6
Markdown==3.7
MarkupSafe==3.0.2
mccabe==0.7.0
mergedeep==1.3.4
mkdocs==1.6.1
mkdocs-get-deps==0.2.0
mkdocs-material==9.6.10
mkdocs-material-extensions==1.3.1
mypy==1.15.0
mypy-extensions==1.0.0
packaging==24.2
paginate==0.5.7
pathspec==0.12.1
platformdirs==4.3.7
pluggy==1.5.0
pycodestyle==2.13.0
pycparser==2.22
pyflakes==3.3.2
Pygments==2.19.1
pymdown-extensions==10.14.3
pytest==8.3.5
pytest-asyncio==0.26.0
pytest-cov==6.0.0
python-dateutil==2.9.0.post0
PyYAML==6.0.2
pyyaml_env_tag==0.1
requests==2.32.3
rfc3986==1.5.0
six==1.17.0
tomli==2.2.1
trustme==1.2.1
typing_extensions==4.13.0
urllib3==2.3.0
uvicorn==0.34.0
watchdog==6.0.0
zipp==3.21.0

name: httpx
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:
      - autoflake==2.3.1
      - babel==2.17.0
      - backrefs==5.8
      - black==25.1.0
      - brotlipy==0.7.0
      - certifi==2025.1.31
      - cffi==1.17.1
      - chardet==3.0.4
      - charset-normalizer==3.4.1
      - click==8.1.8
      - colorama==0.4.6
      - coverage==7.8.0
      - cryptography==44.0.2
      - exceptiongroup==1.2.2
      - flake8==7.2.0
      - ghp-import==2.1.0
      - h11==0.8.1
      - h2==3.2.0
      - hpack==3.0.0
      - hyperframe==5.2.0
      - idna==2.10
      - importlib-metadata==8.6.1
      - iniconfig==2.1.0
      - isort==6.0.1
      - jinja2==3.1.6
      - markdown==3.7
      - markupsafe==3.0.2
      - mccabe==0.7.0
      - mergedeep==1.3.4
      - mkdocs==1.6.1
      - mkdocs-get-deps==0.2.0
      - mkdocs-material==9.6.10
      - mkdocs-material-extensions==1.3.1
      - mypy==1.15.0
      - mypy-extensions==1.0.0
      - packaging==24.2
      - paginate==0.5.7
      - pathspec==0.12.1
      - platformdirs==4.3.7
      - pluggy==1.5.0
      - pycodestyle==2.13.0
      - pycparser==2.22
      - pyflakes==3.3.2
      - pygments==2.19.1
      - pymdown-extensions==10.14.3
      - pytest==8.3.5
      - pytest-asyncio==0.26.0
      - pytest-cov==6.0.0
      - python-dateutil==2.9.0.post0
      - pyyaml==6.0.2
      - pyyaml-env-tag==0.1
      - requests==2.32.3
      - rfc3986==1.5.0
      - six==1.17.0
      - tomli==2.2.1
      - trustme==1.2.1
      - typing-extensions==4.13.0
      - urllib3==2.3.0
      - uvicorn==0.34.0
      - watchdog==6.0.0
      - zipp==3.21.0
prefix: /opt/conda/envs/httpx

tests/models/test_url.py::test_url_join tests/models/test_url.py::test_url_join_rfc3986
tests/dispatch/test_connection_pools.py::test_keepalive_connections tests/dispatch/test_connection_pools.py::test_differing_connection_keys tests/dispatch/test_connection_pools.py::test_soft_limit tests/dispatch/test_connection_pools.py::test_streaming_response_holds_connection tests/dispatch/test_connection_pools.py::test_multiple_concurrent_connections tests/dispatch/test_connection_pools.py::test_close_connections tests/dispatch/test_connection_pools.py::test_standard_response_close tests/dispatch/test_connection_pools.py::test_premature_response_close tests/dispatch/test_connection_pools.py::test_keepalive_connection_closed_by_server_is_reestablished tests/dispatch/test_connection_pools.py::test_keepalive_http2_connection_closed_by_server_is_reestablished
tests/dispatch/test_http2.py::test_http2_get_request tests/dispatch/test_http2.py::test_http2_post_request tests/dispatch/test_http2.py::test_http2_multiple_requests tests/dispatch/test_http2.py::test_http2_reconnect tests/models/test_url.py::test_idna_url tests/models/test_url.py::test_url tests/models/test_url.py::test_url_eq_str tests/models/test_url.py::test_url_params tests/models/test_url.py::test_url_set tests/test_multipart.py::test_multipart[abc-abc0] tests/test_multipart.py::test_multipart[abc-abc1] tests/test_multipart.py::test_multipart_invalid_key[abc] tests/test_multipart.py::test_multipart_invalid_key[1] tests/test_multipart.py::test_multipart_invalid_key[2.3] tests/test_multipart.py::test_multipart_invalid_key[None] tests/test_multipart.py::test_multipart_invalid_value[1] tests/test_multipart.py::test_multipart_invalid_value[2.3] tests/test_multipart.py::test_multipart_invalid_value[None] tests/test_multipart.py::test_multipart_invalid_value[value3] tests/test_multipart.py::test_multipart_invalid_value[value4] tests/test_multipart.py::test_multipart_file_tuple tests/test_multipart.py::test_multipart_encode
httpx/concurrency.py httpx/dispatch/connection_pool.py httpx/__version__.py httpx/dispatch/http11.py httpx/interfaces.py httpx/dispatch/connection.py httpx/dispatch/http2.py httpx/exceptions.py httpx/models.py
httpx/concurrency.py httpx/dispatch/connection_pool.py httpx/__version__.py httpx/dispatch/http11.py httpx/interfaces.py httpx/dispatch/connection.py httpx/dispatch/http2.py httpx/exceptions.py httpx/models.py