| 256 |
256 |
return exc.failure |
| 257 |
257 |
if isinstance(exc, httpx.ConnectTimeout | httpx.ReadTimeout | httpx.WriteTimeout | httpx.PoolTimeout | asyncio.TimeoutError | TimeoutError): |
| 258 |
258 |
return FailureClass.TIMEOUT |
|
259 |
+ if isinstance(exc, httpx.RemoteProtocolError | httpx.ProtocolError) or exc.__class__.__name__ in ("ProtocolError", "StreamReset", "ConnectionTerminated"): |
|
260 |
+ return FailureClass.TIMEOUT # transient transport-level protocol errors (h2 connection reuse, resets): retry policy, not a mystery |
| 259 |
261 |
if isinstance(exc, httpx.ConnectError): |
| 260 |
262 |
msg = str(exc).lower() |
| 261 |
263 |
if "nodename" in msg or "name or service" in msg or "getaddrinfo" in msg or "temporary failure in name" in msg or "no address" in msg: |
| 355 |
357 |
|
| 356 |
358 |
|
| 357 |
359 |
class Fetcher: |
| 358 |
|
− """Shared httpx client (one per worker process). `get()` raises NotModified / FetchError / BlockedError.""" |
|
360 |
+ """Shared httpx client (one per worker process). `get()` raises NotModified / FetchError / BlockedError. |
|
361 |
+ HTTP/1.1 by default: h2 connection reuse against thousands of heterogeneous origins produced `ConnectionInputs.SEND_SETTINGS in state CLOSED` |
|
362 |
+ protocol errors (2026-09-13); HTTP/2 stays available per Fetcher (`http2=True`).""" |
| 359 |
363 |
|
| 360 |
|
− def __init__(self, *, timeout_s: float | None = None, headers: dict[str, str] | None = None, http2: bool = True, |
|
364 |
+ def __init__(self, *, timeout_s: float | None = None, headers: dict[str, str] | None = None, http2: bool = False, |
| 361 |
365 |
max_connections: int | None = None): |
| 362 |
366 |
self.timeout_s = timeout_s or settings.http_timeout_s |
| 363 |
367 |
self.headers = {"User-Agent": settings.user_agent, |
| 364 |
368 |
|