Skip to content

v0.2.20 — HTTP 418 is an IP ban, not a heavy 429 - #9

Open
WickHunter wants to merge 1 commit into
mainfrom
claude/aster-418-ban-guard
Open

v0.2.20 — HTTP 418 is an IP ban, not a heavy 429#9
WickHunter wants to merge 1 commit into
mainfrom
claude/aster-418-ban-guard

Conversation

@WickHunter

Copy link
Copy Markdown
Owner

What this is, and what it is not

The task was "get hub and Aster ready". The hub already collects and serves Aster — that shipped in v0.2.19 (PR #8) and it is good work. This PR does not rebuild it. It fixes the one thing in that path that would have turned a routine rate limit into a multi-day outage, and it is the only hard requirement of the brief that the shipped code did not meet.

The defect

httpRefusal folded HTTP 418 and 429 into one RateLimitError, so a 418 got the rate-limit treatment: halve the rate, go quiet for 60 s (doubling, capped at fifteen minutes), resume.

On a Binance-family venue — which is what Aster is — those two statuses do not mean the same thing:

meaning right response
429 you are going too fast ease off, come back shortly
418 this IP is already banned say nothing at all

Aster's own manual gives the ban as 2 minutes to 3 days, and is explicit that continuing to send requests is what lengthens it. So the hub resumed inside the ban, on a one-minute tick, extending it, for as long as the operator left the service running.

And the exchanges panel called that COOLING — a state the README tells the operator in as many words is "the collector working, not a fault". An IP ban looked like health, on the one venue where a ban takes history away from every install at once rather than from one.

The bot repo had already settled this exact question the other way and written down why (liqhunter src/venues/rate-limited.ts, v0.86.80):

"418 is deliberately NOT treated as a slow-down: by then the venue is refusing the address outright and a caller that reads it as 'retry shortly' would keep hammering a ban."

The hub and a bot share one IP whenever they share a box. They may not disagree about what a ban is.

The fix

VenueBanError extends RateLimitError — the inheritance is load-bearing in both directions. Every existing caller asks isRateLimit and keeps its "stop the pass, do not count this as FAILING" handling, which was always right for a ban too, so nothing had to be found and updated. It is also the trap: the ban flag is the NARROWER question, so a collector that asks the wider one first classifies every ban as a slow-down and the fix does nothing. That ordering is asserted through the collector, not read off the source, and mutation-verified by swapping the two branches.

A 418 then gets its own everything:

  • Its own wait — 15 min doubling to the venue's documented 3-day maximum, not the rate limiter's 60 s → 15 min ladder, which would have resumed inside the great majority of bans. Retry-After, and a banned until <epoch> deadline in the venue's own error body, are used only to LENGTHEN it, never to shorten. That body parse is not verified against a live Aster ban — this box has never been banned by this venue and will not arrange to be — so it is written to be worthless rather than wrong: an unparseable body leaves the collector on its own schedule, and a hallucinated timestamp cannot talk the hub into probing an active ban.
  • Its own silence — nothing is sent, not even the 15-minute instrument list. The list is a request too, and a request is the thing that extends a ban.
  • Its own stateBANNED, red, outranking COOLING / STALLED / FAILING, because all three of those send an operator looking for the wrong thing. The detail names the ban, the time left, the lowered ceiling, that requests lengthen it, and that this is not the collector working.
  • Its own counter, which stays on the card after the ban lifts — an expired ban otherwise leaves no trace at exactly the moment nobody would think to look for one.
  • A lowered ceiling — rate to the floor, ceiling halved, so hundreds of clean requests cannot creep back to the rate that earned the ban. The escalation does not reset on success: a ban expiring proves the ban expired and says nothing about the cause. Cleared by a restart, which is an operator deciding they have dealt with it.

418 is a ban on all four venues, not just Aster. The other three do not document sending it, so a 418 from one of them is a status this hub does not understand — and the safe reading of a status we do not understand is the one that makes the hub go quiet.

The ban schedule gets its own two env knobs rather than borrowing the rate-limit pair (shortening a rate-limit backoff is reasonable and must not silently shorten how long an IP ban is waited out), and both are floored at the venue's documented minimum ban — a wait under that ends inside every ban there is.

Proof

npx tsc clean, all 14 suites green. New tests/venue-ban.test.mjs, 24 checks. Every mutation tried was caught, by name:

mutation first check red
418 → plain RateLimitError (the defect, restored) 418 is a BAN on every venue here…
ask isRateLimit before isVenueBan THE ORDERING: a ban reaches BANNED, not COOLING
BANNED no longer outranks FAILING THE ORDERING: a ban reaches BANNED, not COOLING
ban ladder capped at the rate-limit maximum (15 m) THE WAIT is the ban ladder…
escalation resets on success THE WAIT is the ban ladder…
creep-back reads opts.requestsPerSecond the creep-back respects the lowered ceiling…
ban deadline not folded into the silence check a banned collector says NOTHING to the venue
a 418 body may SHORTEN the wait the venue's deadline is taken when it is LONGER…

Restores were done by re-applying the inverse edit against recorded sha256 hashes, with npx tsc before each gate — never git checkout.

Unchanged on purpose

  • The wire contract and the signature. v1, the canonical key order, keyId: seed-1 signed by the licence key. No bot-side change is needed for Aster — the bot's seeder is handed venue: olbVenue directly and the envelope's venue is a plain string, Aster is live + klinesPageEndMs so olbVenueCandleSupport passes it, and klinesIncludeForming: true means olbVenueKlineFetcher already strips the forming bar before the exact cross-check. The pending candle-1 key rollout is unrelated to Aster and still gated behind the README's four-step order.
  • The pacing. 1000 rows for 5 weight at 4 req/s = 1200 weight/min against the published 2400/min ceiling — exactly 50%, plus the venue's own x-mbx-used-weight-1m readout backing off at 80%.
  • The forming-bar gates and the interval handling. Two independent gates (dropUnclosed, then notAfterMs inside CandleStore.write), and there is no interval lookup table anywhere in the hub to have a ?? "1m" fallback in — each adapter hard-codes its own 1-minute spelling.

Generated by Claude Code

`httpRefusal` folded 418 and 429 into one `RateLimitError`, so a 418 bought the
rate-limit treatment: halve the rate, go quiet for 60s (doubling, capped at 15
minutes), resume. On a Binance-family venue — which is what Aster is — 429 means
"you are going too fast" and 418 means THIS IP IS ALREADY BANNED, for 2 minutes
to 3 days by the venue's own manual, which is explicit that further requests are
what lengthen it. The hub therefore resumed inside the ban, on a one-minute
tick, extending it, for as long as the service ran — and the panel called that
COOLING, which the README tells the operator is "the collector working, not a
fault". An IP ban looked like health, on the one venue where a ban takes history
away from every install at once.

The bot repo settled this the other way and wrote down why (liqhunter
src/venues/rate-limited.ts, v0.86.80). The hub and a bot share one IP whenever
they share a box; they may not disagree about what a ban is.

- `VenueBanError extends RateLimitError`, so every existing caller keeps its
  "stop the pass, do not count as FAILING" handling. That inheritance is also
  the trap: the ban flag is the NARROWER question, so the collector must ask it
  first. Asserted through the collector, mutation-verified by swapping branches.
- Its own wait: 15m doubling to the documented 3-day maximum, not the rate
  limiter's 60s-to-15m ladder. `Retry-After` and a `banned until <epoch>` body
  are used only to LENGTHEN it, never to shorten.
- Its own silence: nothing sent at all, not even the instrument list.
- Its own state (BANNED, red, outranks COOLING/STALLED/FAILING) and its own
  lifetime counter, which stays on the card after the ban lifts.
- Rate to the floor, ceiling halved, and the escalation does not reset on a
  success — cleared by a restart, which is an operator deciding they dealt with
  the cause.
- 418 is a ban on all four venues: the other three do not document sending it,
  so it is a status we do not understand, and the safe reading is silence.
- Own env knobs (HUB_CANDLE_BAN_COOLDOWN_MS / _MAX_), floored at the venue's
  documented minimum ban.

tests/venue-ban.test.mjs — 24 checks; all eight mutations tried caught by name.
npx tsc clean, all 14 suites green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AWoRUxBefVAq5b23sZmZdC
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants