Repository: encode/httpx

Issue:
Deletion of non-existent keys from `Headers` should raise KeyError.
Prompted by  #310

To replicate:

```python
headers = httpx.Headers()
del headers['DOES_NOT_EXIST']  # Should raise a KeyError, but doesn't
```

To resolve this issue we'll need to:

* Ensure that deletion of non-existant keys raises a KeyError.
* Ensure that we have a `.pop(key, default)` that we can use instead for removal without KeyErrors.
* Replace instances of `del headers[key]` in our codebase with `headers.pop(key, None)` where appropriate.


Discussion:

Patch:
diff --git a/httpx/dispatch/http2.py b/httpx/dispatch/http2.py
index 786110c..fd40964 100644
--- a/httpx/dispatch/http2.py
+++ b/httpx/dispatch/http2.py
@@ -4,7 +4,7 @@ import typing
 import h2.connection
 import h2.events
 
-from ..concurrency.base import BaseStream, ConcurrencyBackend, TimeoutFlag, BaseEvent
+from ..concurrency.base import BaseEvent, BaseStream, ConcurrencyBackend, TimeoutFlag
 from ..config import TimeoutConfig, TimeoutTypes
 from ..models import AsyncRequest, AsyncResponse
 from ..utils import get_logger
diff --git a/httpx/middleware.py b/httpx/middleware.py
index dd23ac2..c98c85e 100644
--- a/httpx/middleware.py
+++ b/httpx/middleware.py
@@ -147,15 +147,14 @@ class RedirectMiddleware(BaseMiddleware):
         if url.origin != request.url.origin:
             # Strip Authorization headers when responses are redirected away from
             # the origin.
-            del headers["Authorization"]
+            headers.pop("Authorization", None)
             headers["Host"] = url.authority
 
         if method != request.method and method == "GET":
             # If we've switch to a 'GET' request, then strip any headers which
             # are only relevant to the request body.
-            del headers["Content-Length"]
-            del headers["Transfer-Encoding"]
-
+            headers.pop("Content-Length", None)
+            headers.pop("Transfer-Encoding", None)
         return headers
 
     def redirect_content(self, request: AsyncRequest, method: str) -> bytes:
diff --git a/httpx/models.py b/httpx/models.py
index 59f543f..d0bbc41 100644
--- a/httpx/models.py
+++ b/httpx/models.py
@@ -473,6 +473,8 @@ class Headers(typing.MutableMapping[str, str]):
         for idx, (item_key, _) in enumerate(self._list):
             if item_key == del_key:
                 pop_indexes.append(idx)
+        if not pop_indexes:
+            raise KeyError(key)
 
         for idx in reversed(pop_indexes):
             del self._list[idx]
@@ -624,7 +626,6 @@ class AsyncRequest(BaseRequest):
             assert hasattr(data, "__aiter__")
             self.is_streaming = True
             self.content_aiter = data
-
         self.prepare()
 
     async def read(self) -> bytes:


Test patch:
diff --git a/tests/client/test_headers.py b/tests/client/test_headers.py
index 2d17c12..c5429a8 100755
--- a/tests/client/test_headers.py
+++ b/tests/client/test_headers.py
@@ -2,6 +2,8 @@
 
 import json
 
+import pytest
+
 from httpx import (
     AsyncDispatcher,
     AsyncRequest,
@@ -11,6 +13,7 @@ from httpx import (
     TimeoutTypes,
     VerifyTypes,
     __version__,
+    models,
 )
 
 
@@ -122,3 +125,9 @@ def test_header_update():
             "user-agent": "python-myclient/0.2.1",
         }
     }
+
+
+def test_header_does_not_exist():
+    headers = models.Headers({"foo": "bar"})
+    with pytest.raises(KeyError):
+        del headers["baz"]