Skip to content

refactor: rate limiter and add route preloading support - #879

Merged
aXenDeveloper merged 11 commits into
canaryfrom
feat/rate-limit-budgets-preload
Oct 15, 2026
Merged

aXenDeveloper merged 11 commits into
canaryfrom
feat/rate-limit-budgets-preload

Conversation

@aXenDeveloper

Copy link
Copy Markdown
Owner

Description

What?

This PR makes two major changes to VitNode:

  1. Rate Limiter Refactoring: Splits the monolithic rate limiter into separate budgets for safety, read, write, auth, and content-write operations. Introduces a new RateLimiterConfig interface and rate-limiter-config.ts module to manage configuration. The middleware now:

    • Tracks per-IP and per-user (when authenticated) rate limits separately
    • Applies category-specific budgets based on HTTP method and path
    • Returns X-RateLimit-Category header to identify which budget was exhausted
    • Supports Redis-backed distributed rate limiting
    • Maintains backward compatibility with legacy config options
  2. Route Preloading: Adds support for controlling whether routes preload on link hover via a preload boolean option. Includes:

    • New HrefLink component and useHrefNavigate hook for internal navigation
    • rateLimitPathTemplate() function to anonymize paths in logs
    • Route tree support for preload option with inheritance from parent layouts
    • Comprehensive test coverage for preload behavior

Why?

Rate Limiter: The previous single-budget approach couldn't distinguish between different request types. The new multi-budget system provides:

  • Better protection for sensitive endpoints (auth, sign-up) with stricter limits
  • Fairer resource allocation (reads are cheaper than writes)
  • Per-user budgets for authenticated members to prevent office-wide IP sharing issues
  • Clearer debugging with category headers showing which limit was hit

Preloading: Link preloads can trigger expensive API calls when visitors sweep across lists. The new preload option lets routes opt out, reducing unnecessary API traffic and rate limit consumption during casual browsing.

Test Plan

  • Added comprehensive test suite for rate limiter with multiple budget scenarios, IPv6 aggregation, per-user tracking, and Redis fallback
  • Added tests for HrefLink component navigation behavior
  • Added tests for route preload inheritance and configuration
  • Added tests for query client dehydration with preload metadata
  • Existing tests pass with updated rate limiter configuration

https://claude.ai/code/session_01F1sfMPmQoAgrC8t5g5Kk3n

claude added 10 commits October 14, 2026 20:48
…d content-write budgets under a safety cap

Reads (GET/HEAD) and mutations now spend independent budgets, so route
preloading can no longer use up the allowance a visitor needs to submit a
form. A global safety cap counts every request, including OPTIONS
preflights. Legacy top-level rateLimiter options map onto the safety cap
with their old defaults, so existing installs keep their protection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F1sfMPmQoAgrC8t5g5Kk3n
…load

The app and starter router wait 250ms of hover before preloading, so a
pointer sweeping across a list no longer runs a loader for every link.
page(), index() and layout() accept preload: boolean, inherited through
layouts, which maps to TanStack's route-level preload so a hover skips that
route's loader. Guards still run on every navigation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F1sfMPmQoAgrC8t5g5Kk3n
…che-backed loaders

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F1sfMPmQoAgrC8t5g5Kk3n
Blog and Forum loaders called the fetcher directly, so every hover preload
and the click after it re-sent their API requests. They now warm per-feature
queryOptions with deliberate stale times, so repeat preloads and hover-then-
click share one request. Answers that differ by viewer (hidden records,
private responses, a member's submissions) are never reused across users,
and forum writes invalidate the lists, threads and stats they change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F1sfMPmQoAgrC8t5g5Kk3n
…derData

Blog and Forum loaders read through TanStack Query and also return the data
as loaderData, so the server-rendered HTML carried it twice. Queries marked
with LOADER_DATA_QUERY_META are now left out of the SSR dehydration that
createVitNodeQueryClient configures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F1sfMPmQoAgrC8t5g5Kk3n
…s after preflights

A signed-in member's read, write and content-write budgets are now keyed by
their user ID once globalMiddleware has verified the session, so people
behind one office IP no longer share them and a member cannot refresh them
by switching networks. Guests and unverified cookies stay keyed by IP, and
the safety cap and sign-in budget always are. rateLimiter.perUser: false
restores IP-only keys.

A preflight is still counted against the safety cap but never refused: the
real request that follows gets the 429, which runs after cors() and so
carries CORS headers. Retry-After and X-RateLimit-Category are exposed to
cross-origin pages, merged into any custom cors.exposeHeaders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F1sfMPmQoAgrC8t5g5Kk3n
…le preflight 429s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F1sfMPmQoAgrC8t5g5Kk3n
…nt navigation

Plugin pages rendered internal navigation as plain <a href>, so the router
never preloaded or client-navigated it and the Forum's query cache never
paid off. HrefLink (@vitnode/core/tanstack/link) turns an href string with
query and hash into a router Link, so plugins keep using strings without
importing the host route tree; useHrefNavigate does the same from code.
AdminLink now builds on it, which also fixes admin hrefs with a query.
Creating a topic invalidates the forum queries and navigates client-side
instead of reloading the page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F1sfMPmQoAgrC8t5g5Kk3n
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-15T10:59:41.339806Z dce5ddd New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

React Doctor found 1 new issue in 1 file · 1 warning · score 66 / 100 (Needs work) · 0 fixed · vs canary

1 warning

src/api/middlewares/rate-limiter.middleware.ts

  • ⚠️ L322 await inside a loop async-await-in-loop

Reviewed by React Doctor for commit dce5ddd. See inline comments for fixes.

@aXenDeveloper aXenDeveloper changed the title Refactor rate limiter and add route preloading support refactor: rate limiter and add route preloading support Oct 15, 2026
@github-actions github-actions Bot added the 🧱 Refactor A code change that neither fixes a bug nor adds a feature label Oct 15, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d80e4cc799

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/vitnode/src/lib/query-client.ts Outdated
…larity

refactor(api): ✏️ clean up comments in rate limiter and auth cookie files
refactor(tanstack): ✏️ simplify HrefLink and preload logic in plugin routes
refactor(tests): ✏️ remove unnecessary comments in test files for clarity
: ip;

try {
await limiters[category].limiter.consume(key);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

React Doctor · react-doctor/async-await-in-loop (warning)

This for…of loop waits before starting the next iteration. If iterations perform independent asynchronous work, consider bounded concurrency; await syntax alone does not establish a speedup.

Fix → Consider concurrent calls only for independent asynchronous work. Shared queues or synchronous work may not benefit. Preserve resource limits, transaction ordering, and failure/cancellation semantics; observe all callback promises.

Docs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dce5ddd7aa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +353 to +355
carriesSession(c)
? categories.filter(category => !PER_USER_CATEGORIES.has(category))
: categories,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Do not trust cookie presence before applying IP limits

When perUser is enabled, any nonempty session cookie causes the read/write/content-write IP limits to be skipped here, even though the cookie is not verified until globalMiddleware calls SessionModel.getUser(). An attacker can therefore attach a forged cookie and force up to the 1,000-request safety budget through session hashing/device and database lookups before identified eventually falls back to the IP limit, bypassing the intended 300-read or 60-write protection around that expensive work. Apply a provisional IP category limit until a valid user identity has actually been established, then transfer/refund that consumption for authenticated users.

Useful? React with 👍 / 👎.

@aXenDeveloper
aXenDeveloper merged commit 07cf293 into canary Oct 15, 2026
7 checks passed
@aXenDeveloper
aXenDeveloper deleted the feat/rate-limit-budgets-preload branch October 15, 2026 12:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🧱 Refactor A code change that neither fixes a bug nor adds a feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants