Skip to content

fix(refusals): treat a 403 about the resource or the account as Cloudflare's own error - #365

Merged
mattzcarey merged 3 commits into
mainfrom
fix/refusal-error-codes
Oct 29, 2026
Merged

mattzcarey merged 3 commits into
mainfrom
fix/refusal-error-codes

Conversation

@mattzcarey

@mattzcarey mattzcarey commented Oct 28, 2026 •

Copy link
Copy Markdown
Member

A 403 now counts as a permission refusal only when Cloudflare's errors say the credential was refused. Before, every 401 and 403 got a permissions explanation, and could trigger an insufficient_scope step-up. Fixes #364.

I measured what Cloudflare sends with a throwaway API token that could only read account settings:

What refused Status Error
KV, D1, Workers AI, Browser Rendering, Workflows 401 10000: Authentication error
R2, Queues, Vectorize, Pages, Images, Hyperdrive, AI Gateway, DNS, Zaraz 403 10000: Authentication error
a zone 403 9109: Unauthorized to access requested resource
Workers scripts 403 No access to the specified resource. (no code)

The 403s that aren't about the credential carry their own codes: Zaraz 403: Forbidden: workflow is not preview, Images 5403 on an account without the subscription, Workers for Platforms 10121, SSL for SaaS 1404.

So a 401 is still always a refusal. A 403 is one when its body has no errors, when an error has code 10000 or 9109, or when an error has no code. A 403 whose errors all carry other codes now passes through as Cloudflare's own error, with no explanation and no step-up. For execute, GlobalOutbound now reads a 403's body to decide; 403 bodies are small.

Tests cover the Zaraz and Images answers in execute and the endpoint tools: with and without the scope, plus a mixed 5403 and 10000 answer, which still refuses. Three of them fail without the change.
Why the status is mixed: the edge API gateway (cloudflare/api/gateway) answers every authorization failure with 401 10000 (src/middleware/auth.ts), and the core gateway answers 403. In production the edge serves KV, D1, Workers AI, Browser Rendering and Workflows, the five products that answered 401 above. The gateway's other errors are 403 10000 for dashboard sessions and 404 7000, so every credential refusal from either gateway carries 10000 or 9109, and this rule keeps all of them. The proposed move to 403 (wiki) keeps 10000, so it needs no change here.

Because of that, the second commit also changes what a 401 says when the connection already holds an accepted scope. It used to say Cloudflare rejected the credential and to reconnect. From the edge gateway it is usually the user's role, so it now says a 401 can mean either, and to reconnect only if other requests fail with 401 too.
The third commit fills two gaps in how execute explains missing permissions:

  • GraphQL. The Analytics API answers a missing permission with 200 and data: null, which execute reported as a plain GraphQL error. Measured with a token without analytics permissions, a zone dataset says "does not have permission 'com.cloudflare.api.account.zone.analytics.read'", and an account dataset says "authorization denied". The sandbox now marks those two errors, and the host explains them as a refusal of Analytics Read or Account Analytics Read. For an OAuth connection missing the scope, it steps up for analytics.read or account-analytics.read. GraphQL POSTs count as reads for replay safety, since the API has no mutations. Other GraphQL errors are unchanged.
  • An account in the path. The unknown-account hint covered only the account_id argument. When code puts an account the session can't use straight into a refused request's path, the error now names the session's accounts.

@mattzcarey
mattzcarey merged commit 68c7ec0 into main Oct 29, 2026
5 checks passed
@mattzcarey
mattzcarey deleted the fix/refusal-error-codes branch October 29, 2026 11:42
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.

403s about state or entitlements get a permissions explanation and can trigger step-up

1 participant