Skip to content

fix(octopus): back off when the CDN blocks the token mint - #4849

Merged
springfall2008 merged 3 commits into
mainfrom
fix/octopus-token-mint-edge-block-backoff
Aug 30, 2026
Merged

fix(octopus): back off when the CDN blocks the token mint#4849
springfall2008 merged 3 commits into
mainfrom
fix/octopus-token-mint-edge-block-backoff

Conversation

@mgazza

@mgazza mgazza commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator

Problem

async_graphql_query already handles a 403 that carries a CDN block page: it recognises the body via is_edge_block_body and keeps the cached JWT rather than discarding it and immediately re-minting.

async_refresh_token has no equivalent handling, and unlike a query it has no cached result to fall back on — once the JWT expires, every authenticated call needs a new one.

So one edge block landing near token expiry is enough to lock an install out permanently:

  1. The mint is refused with a 403 CDN page.
  2. So is the retry, and the one after that.
  3. The component re-mints on every poll, forever. Observed in the wild: ~50 mint attempts an hour for 26 hours, zero successes, no self-heal. Only a restart clears it, and even that re-mints straight back into the block.

It is also well hidden. REST rate downloads keep working throughout, so import/export rates stay correct and the entity keeps serving last-known device settings — while every dispatch, settings and saving-session query silently returns nothing.

And it is easy to misdiagnose: the block is logged as the generic

Warn: OctopusAPI: Response received - .../v1/graphql/ (refresh-token) - Unauthenticated request: 403; <!DOCTYPE HTML ...
Warn: OctopusAPI: Failed to retrieve auth token

which reads like a revoked API key. A genuinely bad key looks quite different — HTTP 200 with a null obtainKrakenToken, and no 403 line at all.

Change

  • Detect the CDN block in async_refresh_token using the existing is_edge_block_body check.
  • Back off exponentially instead of re-minting: 5 minutes doubling to a 1 hour cap. A block that lifts is still picked up within the hour; a block that does not is no longer hammered.
  • Suppress the request entirely while the backoff window is open — no socket, no log spam.
  • Clear the backoff on the next successful mint.
  • Log it as an edge block, so it is not triaged as a credential problem.

A 403 that is not identifiable as a CDN page keeps the existing refresh-and-retry behaviour, so genuinely revoked credentials still recover without a restart.

Tests

Four new cases in tests/test_octopus_refresh_token.py (Tests 9-12):

  • a CDN/WAF 403 on the mint starts a backoff and short-circuits before the generic response reader
  • no HTTP request is made at all while the backoff is active, and a suppressed attempt does not grow it
  • the backoff grows per block, is capped, and is cleared by a successful mint
  • a non-CDN (JSON) 403 does not start a backoff and still goes through the normal path

python3 unit_test.py -k octopus passes in full, including the existing octopus_waf_block and octopus_refresh_token suites.

mgazza and others added 2 commits August 30, 2026 00:51
A 403 carrying a CDN block page is already handled for GraphQL queries, which
keep their cached JWT rather than discarding it. The token mint sits behind the
same CDN but had no such handling, and unlike a query it has no cached result to
fall back on: once the JWT expires, every authenticated call needs a new one.

The result is a lockout that cannot self-heal. One edge block near token expiry
is enough - the mint is refused, so is the retry, and the component re-mints on
every poll indefinitely (~50 attempts an hour observed over 26 hours, with no
successes). Rates keep downloading over REST throughout, so the integration
looks healthy while every dispatch, settings and saving-session query silently
returns nothing.

Detect the block in async_refresh_token with the existing is_edge_block_body
check and back off exponentially instead, 5 minutes doubling to a 1 hour cap,
cleared on the next successful mint. A block that lifts is still picked up
within the hour, but a block that does not is no longer hammered.

The block is also now logged as an edge block rather than as the generic
"Unauthenticated request: 403", which reads like a revoked API key and sends
anyone diagnosing this down the wrong path. A 403 that is not identifiable as a
CDN page keeps the existing refresh-and-retry behaviour, so genuinely revoked
credentials still recover.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NnzhfR8uDgGUtYLNpDA4ka
… off

Review catch on the previous commit. The backoff guard sat after the five minute
proactive-refresh check, so a token with four minutes of genuine life left was
treated as unusable the moment a mint was refused: one transient CDN 403 could
cost up to five minutes of queries that would otherwise have succeeded. Before
the backoff existed the next poll would simply re-mint and recover, so this was
a regression the block itself did not cause.

Decode the expiry once and, while backing off, hand back a token that has not
actually expired. An expired or undecodable token still returns None. Callers
that hit a genuine auth failure clear graphql_token before re-minting, so this
can only revive a token we have not been able to renew yet, never a revoked one.

Also from review:

- Repeat the backoff reason at most every 10 minutes while suppressed. The mint
  makes no request during the window and so logged nothing, leaving anyone
  reading a short log window unable to tell a deliberate cooldown from a bad API
  key - every caller only says "token refresh failed".
- Clamp the doubling exponent so a long block cannot evaluate 2 ** block_count
  without bound. The delay was already capped; this bounds the arithmetic.
- Extract the schedule into token_mint_backoff_seconds().

Tests: assert the exact backoff schedule rather than mere monotonicity (a
1, 2, 2, 2... implementation passed the old check), and add two cases the
previous ones could not catch - an elapsed deadline reopening the mint with the
deadline left set, so a guard that tests the field for presence rather than
against the clock fails, and a near-expiry token still being served during a
backoff. Both were confirmed to fail against the corresponding mutation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NnzhfR8uDgGUtYLNpDA4ka
Second review pass. The previous commit only handled the backoff guard, so the
call that actually met the CDN 403 still returned None even when the token it
held had not expired - the fallback started one call late, for no reason. The
guard would have handed that same token to the very next caller anyway.

Return it from the edge-block branch too, re-reading the clock rather than
reusing the timestamp sampled before the request went out.

Tests: Test 14 asserted the old behaviour, so it was pinning the defect - it now
requires the token from the triggering call. Adds an undecodable token during an
active backoff (no provable life left, so it must not be served) and a throttle
test proving the reason is restated once per interval rather than per call.
Both new assertions were confirmed to fail against the corresponding mutation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NnzhfR8uDgGUtYLNpDA4ka

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

A few small but concrete docstring/typo and PR-description mismatches were found that should be corrected before merge.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds CDN/WAF edge-block handling to Octopus token minting so installs don’t get stuck in an endless re-mint loop when the CDN serves a 403 block page near JWT expiry. This extends the existing is_edge_block_body behavior from GraphQL queries to async_refresh_token, introducing exponential backoff and clearer logging.

Changes:

  • Add token-mint backoff constants/helpers and per-instance backoff state in OctopusAPI.
  • Detect CDN/WAF 403 bodies during token mint, suppress repeated mints during a backoff window, and clear backoff on successful mint.
  • Expand async_refresh_token tests to cover edge-block backoff behavior, suppression, cap/clear behavior, and log throttling.
File summaries
File Description
apps/predbat/octopus.py Implements token-mint CDN/WAF detection plus exponential backoff + log throttling and backoff state handling.
apps/predbat/tests/test_octopus_refresh_token.py Adds/extends unit tests validating backoff start/suppression/growth/cap/clear and logging behavior.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 3
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines 26 to +30
- Test 5: Token refresh handles API failure gracefully
- Test 6: Token refresh handles timeout gracefully
- Test 7: Token expiry decoding from JWT
- Test 9: A CDN/WAF 403 on the mint backs off instead of re-minting every poll
- Test 10: No HTTP request is made at all while the mint backoff is active
Comment thread apps/predbat/octopus.py


def token_mint_backoff_seconds(block_count):
"""Backoff delay in seconds for the block_count'th consecutive CDN block on a token mint.
Comment on lines +29 to +35
- Test 9: A CDN/WAF 403 on the mint backs off instead of re-minting every poll
- Test 10: No HTTP request is made at all while the mint backoff is active
- Test 11: Backoff grows per block, is capped, and a success clears it
- Test 12: A non-CDN 403 does not start a backoff
- Test 13: An elapsed backoff deadline reopens the mint without touching the state
- Test 14: A token inside the proactive-refresh window is still used while backing off
- Test 15: The backoff reason is repeated but throttled
@springfall2008
springfall2008 merged commit ee07f82 into main Aug 30, 2026
3 checks passed
@springfall2008
springfall2008 deleted the fix/octopus-token-mint-edge-block-backoff branch August 30, 2026 08:28
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.

3 participants