Skip to content

Commit 820addc

Browse files
lesnik512claude
andcommitted
refactor: hoist STREAMING_BODY_MARKER + _STREAMING_BODY_REFUSAL_NOTE to module constants
Final-review feedback: the "httpware.streaming_body" marker key was duplicated at 5 sites (1 write in client.py + 4 reads in retry.py) and the PEP-678 refusal note was duplicated at 4 sites in retry.py. Per project convention (module-level UPPER_CASE constants over inlined string literals; same pattern as _MAX_ATTEMPTS_INVALID, _MAX_CONCURRENT_INVALID, _DEFAULT_DECODER_MISSING_MESSAGE), hoist both: - STREAMING_BODY_MARKER in client.py (public so retry.py can import it; this IS the contract between the two modules) - _STREAMING_BODY_REFUSAL_NOTE in retry.py (private; only used there) retry.py now imports STREAMING_BODY_MARKER from client. No circular import: client doesn't import retry transitively. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4c18956 commit 820addc

2 files changed

Lines changed: 18 additions & 17 deletions

File tree

src/httpware/client.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,13 @@ def _raise_on_status_error(response: httpx2.Response) -> None:
7171
raise exc_class(response)
7272

7373

74+
STREAMING_BODY_MARKER = "httpware.streaming_body"
75+
"""Key set on ``httpx2.Request.extensions`` by ``_request_with_body`` when content/data/files is an async-iterable.
76+
77+
``Retry.__call__`` reads this marker to refuse retrying a streamed-body request
78+
(the consumed iterator cannot replay across attempts)."""
79+
80+
7481
def _is_streaming_body(value: typing.Any) -> bool:
7582
"""Return True if value is an async-iterable that cannot be safely replayed for retry."""
7683
if value is None:
@@ -210,7 +217,7 @@ async def _request_with_body( # noqa: PLR0913, C901 — mirrors httpx2 per-meth
210217
kwargs["files"] = files
211218
request = self._httpx2_client.build_request(method, url, **kwargs)
212219
if _is_streaming_body(content) or _is_streaming_body(data) or _is_streaming_body(files):
213-
request.extensions["httpware.streaming_body"] = True
220+
request.extensions[STREAMING_BODY_MARKER] = True
214221
return await self.send(request, response_model=response_model)
215222

216223
@typing.overload

src/httpware/middleware/resilience/retry.py

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import httpx2
1818

19+
from httpware.client import STREAMING_BODY_MARKER
1920
from httpware.errors import NetworkError, RetryBudgetExhaustedError, StatusError, TimeoutError # noqa: A004
2021
from httpware.middleware import Next
2122
from httpware.middleware.resilience._backoff import full_jitter_delay
@@ -43,6 +44,7 @@
4344
)
4445

4546
_MAX_ATTEMPTS_INVALID = "max_attempts must be >= 1"
47+
_STREAMING_BODY_REFUSAL_NOTE = "httpware: not retrying — request body is a stream that cannot replay across attempts"
4648

4749

4850
def _parse_retry_after(value: str) -> float | None:
@@ -108,42 +110,34 @@ async def __call__(self, request: httpx2.Request, next: Next) -> httpx2.Response
108110
except StatusError as exc:
109111
retryable_status = exc.response.status_code in self.retry_status_codes
110112
if not method_eligible or not retryable_status:
111-
if retryable_status and request.extensions.get("httpware.streaming_body"):
112-
exc.add_note(
113-
"httpware: not retrying — request body is a stream that cannot replay across attempts"
114-
)
113+
if retryable_status and request.extensions.get(STREAMING_BODY_MARKER):
114+
exc.add_note(_STREAMING_BODY_REFUSAL_NOTE)
115115
raise
116116
last_exc = exc
117117
last_response = exc.response
118118
except (NetworkError, TimeoutError) as exc:
119119
if not method_eligible:
120-
if request.extensions.get("httpware.streaming_body"):
121-
exc.add_note(
122-
"httpware: not retrying — request body is a stream that cannot replay across attempts"
123-
)
120+
if request.extensions.get(STREAMING_BODY_MARKER):
121+
exc.add_note(_STREAMING_BODY_REFUSAL_NOTE)
124122
raise
125123
last_exc = exc
126124
last_response = None
127125
except builtins.TimeoutError as exc:
128126
wrapped = TimeoutError("attempt timed out")
129127
wrapped.__cause__ = exc # set now; the retry path (last_exc = wrapped) has no `from` clause
130128
if not method_eligible:
131-
if request.extensions.get("httpware.streaming_body"):
132-
wrapped.add_note(
133-
"httpware: not retrying — request body is a stream that cannot replay across attempts"
134-
)
129+
if request.extensions.get(STREAMING_BODY_MARKER):
130+
wrapped.add_note(_STREAMING_BODY_REFUSAL_NOTE)
135131
raise wrapped from exc
136132
last_exc = wrapped
137133
last_response = None
138134

139135
# ---- retryable failure path
140-
if request.extensions.get("httpware.streaming_body"):
136+
if request.extensions.get(STREAMING_BODY_MARKER):
141137
if last_exc is None: # pragma: no cover — invariant from except branch
142138
msg = "Retry: streaming-body refusal reached with no last_exc"
143139
raise AssertionError(msg)
144-
last_exc.add_note(
145-
"httpware: not retrying — request body is a stream that cannot replay across attempts"
146-
)
140+
last_exc.add_note(_STREAMING_BODY_REFUSAL_NOTE)
147141
raise last_exc
148142

149143
if is_last:

0 commit comments

Comments
 (0)