Skip to content

fix(notion): bypass Cloudflare IP block via loadCachedPageChunkV2 + throttle/cache - #768

Merged
hkirat merged 4 commits into
mainfrom
fix/notion-cloudflare-block
Aug 5, 2026
Merged

fix(notion): bypass Cloudflare IP block via loadCachedPageChunkV2 + throttle/cache#768
hkirat merged 4 commits into
mainfrom
fix/notion-cloudflare-block

Conversation

@hkirat

@hkirat hkirat commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

https://projects.100xdevs.com/tracks/... was returning HTTP 500 on every track/problem page (root still 200). The k8s cluster is healthy — the failure is an external dependency.

Root cause: track/problem content is fetched server-side from Notion's unofficial /api/v3 API via notion-client. Notion sits behind Cloudflare, and requests from our DigitalOcean datacenter egress IP are hard-blocked with a 403 "Attention Required" Cloudflare page on the endpoints notion-client uses (loadPageChunk, syncRecordValues, queryCollection). That 403 became a Next.js 500.

Verified: a valid token_v2 returns 200 from a residential IP but still 403 from the pod — Cloudflare blocks at the edge on IP reputation, before the cookie is checked. So auth alone does not fix it.

Key finding: loadCachedPageChunkV2 (and getRecordValues) are not blocked from the cluster and loadCachedPageChunkV2 returns the full page recordMap in a single request.

Changes

  • New getNotionClient() (auth via NOTION_TOKEN_V2 / NOTION_ACTIVE_USER), used at all call sites (tracks, pdf, search, AddTracks).
  • fetchNotionPage now fetches through loadCachedPageChunkV2 (via the client's public fetch()) instead of the blocked getPage().
  • Throttling: global concurrency cap (2) + min-gap between requests + exponential backoff that's harder on 403s. The block is IP-reputation based and re-triggers under bursts (the PDF route fetches every problem of a track in parallel), so keeping volume low is what keeps the endpoint working.
  • Caching + graceful fallback: 30-min fresh cache + never-expiring stale fallback, so a transient block serves slightly-stale content instead of a 500.
  • NotionRenderer guards against an empty/missing recordMap; search/AddTracks guard against null.
  • Documented NOTION_TOKEN_V2 / NOTION_ACTIVE_USER in .env.example and turbo.json.

Deploy note

Set NOTION_TOKEN_V2 (the token_v2 cookie from a logged-in notion.so session) in dailycode-secret, then rebuild/redeploy dailycode. Auth is optional for the endpoint swap to work, but recommended for private-page access.

Known limitation: Notion-hosted images (getSignedFileUrls) and embedded databases (queryCollection) use endpoints that are still IP-blocked, so those may render degraded. Text/code content (the bulk of the course) is restored. The fully-robust follow-up is routing Notion egress through a clean/non-datacenter IP (proxy).

Test plan

  • turbo build passes
  • With deploy, /tracks/Bridges/Bridges-1 returns 200 with content
  • Simulate a Notion failure → page serves stale/fallback instead of 500
  • Load several track pages in quick succession → no 403 storm (throttle holds)

Generated with Devin

Update: built-in proxy support

Also added optional NOTION_PROXY_URL. When set, all Notion calls tunnel through that proxy (via https-proxy-agent), routing egress off the flagged datacenter IP — the fully-robust fix that also restores images (getSignedFileUrls) and embedded DBs (queryCollection). Inert when unset.

hkirat and others added 2 commits August 5, 2026 14:44
Notion's unofficial /api/v3 endpoints (loadPageChunk) are hard-blocked by
Cloudflare with a 403 "Attention Required" page when called from datacenter
egress IPs, which was 500'ing every track/problem page in production.

- Add getNotionClient() that authenticates with NOTION_TOKEN_V2 /
  NOTION_ACTIVE_USER so requests aren't challenged; reuse a singleton.
- Cache recordMaps (10m TTL) with a never-expiring stale fallback so a
  transient Notion/Cloudflare failure degrades to slightly-stale content
  instead of a 500, and to cut the request volume keeping our IP flagged.
- Retry getPage/getBlocks with backoff.
- Guard NotionRenderer against an empty/missing recordMap.
- Document the new env vars in .env.example and turbo.json.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…block

Authenticating alone doesn't help: Cloudflare blocks our datacenter egress IP
at the edge (403) before the token_v2 cookie is even checked, on the endpoints
notion-client uses (loadPageChunk, syncRecordValues, queryCollection).

loadCachedPageChunkV2 is NOT blocked from the cluster and returns the full page
recordMap in one request, so fetch through it via the client's public fetch()
instead of getPage(). The block is IP-reputation based and re-triggers under
request bursts, so add a global concurrency cap + min-gap throttle and back off
harder on 403s; combined with the existing 30m cache + stale fallback this keeps
request volume low enough to stay unblocked. Route search/AddTracks through the
same path and guard against null recordMaps.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@hkirat hkirat changed the title fix(notion): auth + caching to survive Cloudflare 403 blocks fix(notion): bypass Cloudflare IP block via loadCachedPageChunkV2 + throttle/cache Aug 5, 2026
Add opt-in proxy support so all Notion API calls can tunnel through a clean
(non-datacenter) IP via https-proxy-agent when NOTION_PROXY_URL is set. This is
the fully-robust fallback for Notion's IP-reputation-based Cloudflare block and
also covers the endpoints loadCachedPageChunkV2 can't (images/embedded DBs).
Inert when the env var is unset.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@hkirat
hkirat force-pushed the fix/notion-cloudflare-block branch from c44c336 to 2314ed4 Compare August 5, 2026 11:07
@hkirat
hkirat merged commit 25fb590 into main Aug 5, 2026
1 check failed
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.

1 participant