| 256 | return exc.failure |
256 | return exc.failure |
| 257 | if isinstance(exc, httpx.ConnectTimeout | httpx.ReadTimeout | httpx.WriteTimeout | httpx.PoolTimeout | asyncio.TimeoutError | TimeoutError): |
257 | if isinstance(exc, httpx.ConnectTimeout | httpx.ReadTimeout | httpx.WriteTimeout | httpx.PoolTimeout | asyncio.TimeoutError | TimeoutError): |
| 258 | return FailureClass.TIMEOUT |
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 | if isinstance(exc, httpx.ConnectError): |
261 | if isinstance(exc, httpx.ConnectError): |
| 260 | msg = str(exc).lower() |
262 | msg = str(exc).lower() |
| 261 | 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: |
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 | class Fetcher: |
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 | max_connections: int | None = None): |
365 | max_connections: int | None = None): |
| 362 | self.timeout_s = timeout_s or settings.http_timeout_s |
366 | self.timeout_s = timeout_s or settings.http_timeout_s |
| 363 | self.headers = {"User-Agent": settings.user_agent, |
367 | self.headers = {"User-Agent": settings.user_agent, |
| 364 | |
368 | |