You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: planning/deferred-work.md
+4Lines changed: 4 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -4,6 +4,10 @@ Items raised in reviews that are real but not actionable now.
4
4
5
5
## Open
6
6
7
+
### Retry + streaming bodies (Epic 4 interaction)
8
+
9
+
-**`Retry` re-invokes `next(request)` with the same `httpx2.Request` on each attempt.** Safe for in-memory bytes/JSON bodies; unsafe for streaming/async-iterable bodies (consumed iterator can't replay). When Epic 4 ships `AsyncClient.stream` (`4-3`), Retry needs to refuse to retry streamed-body requests (or document that callers supply a body factory). Spec: `planning/specs/2026-06-05-retry-and-retry-budget-design.md` §"Open questions".
10
+
7
11
### Decoder-side
8
12
9
13
-**`_get_adapter``lru_cache` is module-global, not per-decoder instance** — keyed by `model` only; two `PydanticDecoder()` instances with different configurations (none today) would share adapters, and the cache survives across tests unless explicitly cleared. Revisit if/when a configurable `PydanticDecoder(mode=..., strict=...)` lands. (`src/httpware/decoders/pydantic.py:12-14`)
Then create each file with the contents below. Use the Write tool, not bash heredocs.
65
65
66
-
`src/httpware/middleware/resilience/__init__.py`:
66
+
`src/httpware/middleware/resilience/__init__.py` (docstring-only — re-exports defer to Task 7 so intermediate tasks can `import httpware.middleware.resilience.budget` without tripping an import-time `ImportError` from this `__init__.py`):
67
67
```python
68
-
"""Resilience primitives: Retry middleware and RetryBudget token bucket."""
68
+
"""Resilience primitives: Retry middleware and RetryBudget token bucket.
69
69
70
-
from httpware.middleware.resilience.budget import RetryBudget
71
-
from httpware.middleware.resilience.retry import Retry
70
+
Re-exports land in Task 7 once both classes exist; until then this file is
71
+
docstring-only so that importing ``httpware.middleware.resilience.budget``
72
+
during the intermediate tasks does not trip an import-time ``ImportError``.
Edit `src/httpware/errors.py`. Add a new class immediately after the existing `class TransportError`:
138
140
```python
139
141
classNetworkError(TransportError):
140
-
"""Transient network-layer failure (connect/read/write/pool). Safe to retry."""
142
+
"""Transient network-layer failure (connect/read/write/close). Safe to retry.
143
+
144
+
Pool-acquisition timeouts are NOT under this class; they raise ``TimeoutError``
145
+
via ``httpx2.PoolTimeout`` (a ``TimeoutException`` subclass).
146
+
"""
141
147
```
142
148
143
149
Run: `uv run pytest tests/test_errors.py::test_network_error_is_transport_error -v`
@@ -218,7 +224,7 @@ Becomes:
218
224
219
225
The `httpx2.NetworkError` branch must come BEFORE `httpx2.HTTPError` (HTTPError is the broader base). `httpx2.NetworkError` is httpx's documented base for `ConnectError`, `ReadError`, `WriteError`, `PoolTimeout` — if `httpx2`'s symbol name differs (e.g., `httpx2.exceptions.NetworkError`), use whichever import path mirrors the existing `httpx2.ConnectError` import in `tests/test_error_mapping_terminal.py` (which works via top-level `httpx2`).
220
226
221
-
If `httpx2.NetworkError` does not exist, fall back to enumerating the transient subset explicitly: `except (httpx2.ConnectError, httpx2.ReadError, httpx2.WriteError, httpx2.PoolTimeout) as exc:`. The plan author has confirmed `httpx2.ConnectError` and `httpx2.ReadTimeout` already work in the existing tests; the enumeration fallback is safe.
227
+
If `httpx2.NetworkError` does not exist, fall back to enumerating the transient subset explicitly: `except (httpx2.ConnectError, httpx2.ReadError, httpx2.WriteError, httpx2.CloseError) as exc:`. (`PoolTimeout` is NOT in this list — it inherits from `httpx2.TimeoutException` and is already caught by the timeout branch above.) The plan author has confirmed `httpx2.ConnectError` and `httpx2.ReadTimeout` already work in the existing tests; the enumeration fallback is safe.
git commit -m "feat(errors): add NetworkError(TransportError) for transient httpx2 failures
266
272
267
273
Refines _terminal so httpx2.NetworkError-family exceptions (ConnectError, ReadError,
268
-
WriteError, PoolTimeout) map to httpware.NetworkError. InvalidURL and CookieConflict
274
+
WriteError, CloseError) map to httpware.NetworkError. InvalidURL and CookieConflict
269
275
stay bare TransportError. Prerequisite for the Retry middleware so it can retry
270
276
transient failures without retrying typos."
271
277
```
@@ -1029,15 +1035,30 @@ class Retry:
1029
1035
Run: `uv run pytest tests/test_retry.py -v`
1030
1036
Expected: all PASS.
1031
1037
1032
-
-[ ]**Step 4: Lint**
1038
+
-[ ]**Step 4: Wire `Retry` + `RetryBudget` into `resilience/__init__.py`**
1033
1039
1034
-
Run: `uv run ruff check src/httpware/middleware/resilience/retry.py tests/test_retry.py && uv run ty check src/httpware/middleware/resilience/retry.py`
1040
+
Now that both classes exist, replace `src/httpware/middleware/resilience/__init__.py` with:
1041
+
```python
1042
+
"""Resilience primitives: Retry middleware and RetryBudget token bucket."""
1043
+
1044
+
from httpware.middleware.resilience.budget import RetryBudget
1045
+
from httpware.middleware.resilience.retry import Retry
1046
+
1047
+
1048
+
__all__= ["Retry", "RetryBudget"]
1049
+
```
1050
+
1051
+
The `__all__` is required to silence ruff F401 ("imported but unused") and matches the pattern used by `httpware/__init__.py` and `httpware/decoders/__init__.py`.
1052
+
1053
+
-[ ]**Step 5: Lint**
1054
+
1055
+
Run: `uv run ruff check src/httpware/middleware/resilience/ tests/test_retry.py && uv run ty check src/httpware/middleware/resilience/`
1035
1056
Expected: clean. If ruff flags `Callable` / `Awaitable` import paths, adjust per existing project pattern (see `middleware/__init__.py` which uses `from collections.abc import Awaitable, Callable`).
And refines the terminal mapping so that `httpx2`'s transient-network exception family (`httpx2.NetworkError` per httpx convention, or whichever symbols httpx2 exposes for the same hierarchy) raises `httpware.NetworkError` rather than the broader `TransportError`. `InvalidURL` and `CookieConflict` continue to raise `TransportError` directly so they are NOT retried. Existing tests catching `TransportError` keep working (`NetworkError` is a subclass).
0 commit comments