fix: bound 429 retry_after and retry connection errors without a proxy (v0.1.5)
A server-controlled 429 retry_after/Retry-After was slept verbatim with no finiteness check or ceiling, so an inf or huge value (adversarial or a ms-vs-s unit mismatch) could stall send() for hours outside max_retries accounting. Non-finite values are now rejected and finite values clamped to MAX_RETRY_AFTER (300s). Connection/timeout errors with no proxy provider set failed one-shot, contradicting the README's documented "normal retry" behavior and skipping the single most retry-worthy failure class. They now raise the internal _Retryable signal so commons.aretry retries them under max_retries, same as a 5xx, while still returning a failed WebhookResult (never raising) once retries are exhausted. Signed-off-by: disqualifier <dev@disqualifier.me>
This commit is contained in:
@@ -13,16 +13,16 @@ send to the core — inheriting rotation, proxy, retry, and result for free.
|
||||
## Install
|
||||
|
||||
```
|
||||
aiowebhooks @ git+ssh://git@git.rethinkstudios.io/rethink-public/aiowebhooks.git@v0.1.4
|
||||
aiowebhooks @ git+ssh://git@git.rethinkstudios.io/rethink-public/aiowebhooks.git@v0.1.5
|
||||
# discord embeds / identity helpers need the extra:
|
||||
aiowebhooks[discord] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aiowebhooks.git@v0.1.4
|
||||
aiowebhooks[discord] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aiowebhooks.git@v0.1.5
|
||||
```
|
||||
|
||||
The base pulls `aiohttp` and `commons` (for the retry/backoff engine). Only
|
||||
`aiowebhooks[discord]` adds `discord.py` (>=2.3, mainline — not discord.py-self), and
|
||||
only for `DiscordWebhook`.
|
||||
|
||||
Drop the `@v0.1.4` suffix from the line above to install the latest unpinned.
|
||||
Drop the `@v0.1.5` suffix from the line above to install the latest unpinned.
|
||||
|
||||
## Core sender
|
||||
|
||||
@@ -75,8 +75,14 @@ Status retries run through `commons.aretry` (exponential backoff + cap):
|
||||
- **429** — always retried, capped by `max_retries`. When a wait is parseable (body
|
||||
`retry_after` first — Discord sends seconds — then the `Retry-After` header) it sleeps
|
||||
that value before retrying; a 429 with no parseable wait (edge/Cloudflare/generic
|
||||
webhook) still retries under aretry's backoff rather than failing one-shot.
|
||||
webhook) still retries under aretry's backoff rather than failing one-shot. A
|
||||
non-finite value (`inf`/`nan`) is rejected outright and a finite value is clamped to
|
||||
`MAX_RETRY_AFTER` (300s) — a server-controlled wait can never stall a `send()` past
|
||||
that ceiling, however large or malformed the value it sends.
|
||||
- **5xx** — retried with exponential backoff, capped by `max_retries`.
|
||||
- **connection/timeout errors** — retried with exponential backoff, capped by
|
||||
`max_retries`, same as a 5xx (with no proxy provider; see below for the
|
||||
proxy-rotation path).
|
||||
- **4xx** (other than 429) — fails immediately (no retry), returned as `ok=False`.
|
||||
|
||||
Exceeding a cap returns a failed result rather than looping — and the result carries the
|
||||
@@ -104,7 +110,8 @@ On a timeout/connection error the current proxy is burned and the next is tried,
|
||||
`max_proxy_retries`. Hitting the cap, or **any exception from the provider's
|
||||
`get()`/`burn()`** (the provider is duck-typed and never imported, so its exception
|
||||
types can't be caught by class), returns a failed result — never an infinite loop, never
|
||||
an escape. With no provider, a timeout just fails after normal retry.
|
||||
an escape. With no provider, a timeout/connection error is retried directly under
|
||||
`max_retries` (the same cap and backoff a 5xx gets) instead of burn+rotate.
|
||||
|
||||
## Discord (`aiowebhooks[discord]`)
|
||||
|
||||
@@ -141,6 +148,21 @@ Without the extra installed, importing `aiowebhooks` still works; constructing o
|
||||
|
||||
## Changelog
|
||||
|
||||
### v0.1.5
|
||||
|
||||
- **429 `retry_after` bounded:** a non-finite server-controlled wait (`inf`/`nan`, from
|
||||
the body `retry_after` or the `Retry-After` header) is now rejected outright, and a
|
||||
finite wait is clamped to `MAX_RETRY_AFTER` (300s). Previously a bare `float()` parse
|
||||
slept the value verbatim, unbounded and outside `max_retries` accounting — an
|
||||
adversarial or ms-vs-s-misconfigured server could stall a `send()` for hours.
|
||||
- **Connection/timeout errors now retry without a proxy provider:** previously
|
||||
`aiohttp.ClientError`/`asyncio.TimeoutError` with no `proxies=` set failed one-shot,
|
||||
contradicting both the README and the single most retry-worthy failure class. Now it
|
||||
retries under `commons.aretry`'s backoff, capped by `max_retries`, same as a 5xx —
|
||||
and still returns `ok=False` (never raises) once retries are exhausted. Proxy-rotation
|
||||
behavior (burn + rotate, capped by `max_proxy_retries`) is unchanged when a provider
|
||||
is set.
|
||||
|
||||
### v0.1.4
|
||||
|
||||
- **Never-raises net widened:** an unexpected exception that escapes a send attempt (a
|
||||
|
||||
Reference in New Issue
Block a user