Skip to content

[NO-TICKET] Refresh rules on Vercel without waiting out a backoff - #459

Merged
mariojgt merged 1 commit into
mainfrom
mariot/vercel-refresh-waituntil
Oct 28, 2026
Merged

mariojgt merged 1 commit into
mainfrom
mariot/vercel-refresh-waituntil

Conversation

@mariojgt

Copy link
Copy Markdown
Contributor

No ticket.

What changed

On a v0 app published to Vercel, a custom rule removed in Patchstack kept blocking: every request to /?q=attacktest still got a 403 from rule 22327 after the rule was deleted. The status page meanwhile showed the published app fetching rules a few minutes earlier. Rule changes now reach a Vercel app on the normal schedule: the 30-second background check, and at worst the 5-minute refresh a request waits for.

Why the deleted rule stayed

The Next.js middleware the guard is wired into is called by Next with the request alone (middleware(request)), so protection.fetchGuard()(request) had no waitUntil to hand work to. Two things followed:

  • The 30-second background rules check started during a request, and Vercel cut it off as soon as the response went out.
  • Each cut-off check counted as a failed refresh. In the request-refresh scheduler, a failure also pushed back the refresh a request waits for, by up to 8 times the 5-minute interval. So a copy of the app that kept losing its background checks could hold stale rules for about 40 minutes, including a rule deleted since.

Vercel runs many copies of an app. One copy could fetch the new rules (the status page's "fetched 4 minutes ago") while the copies answering visitors still ran the old ones.

Fix

  • src/protect/rules/request-refresh.js:
    • a background check that fails or is cut off now backs off only its own deadline, and the waited refresh still comes due on time;
    • a background check that succeeds counts like any refresh, as before.
  • src/protect/runtime.js: hostWaitUntil also reads Vercel's request context, globalThis[Symbol.for('@vercel/request-context')].get().waitUntil. This is where Vercel's own @vercel/functions package reads it (checked in 3.10.4's get-context.js). Background checks and block reports on Vercel now finish after the response, even when the framework passes the request alone.
  • No template changes, so existing apps pick this up by updating @patchstack/connect and redeploying.

Verified

  • tests/protect/runtime-synchronization.test.ts, two new tests that both fail on the previous code:
    • a background check that fails leaves the waited refresh due at 5 minutes, not 10;
    • with a Vercel request context present, a background check started by fetchGuard()(request) (the request alone, as Next calls it) is handed to that context's waitUntil.
  • npm run typecheck and npm run build are green, and so are the disclosure, capability and install-prompt checks. Full npm test: 4647 passed. Two timing tests timed out under full-suite load and pass when run alone.
  • Not yet checked against the live Vercel app: that needs this release and a redeploy.

Out of scope, worth a follow-up

  • The Next.js templates could pass Next's own event (NextFetchEvent) to the guard instead of relying on Vercel's context, which would also cover Next hosted elsewhere. That is a template change: existing unedited guards would need re-running setup to upgrade, and protect --check compares against the current template. So it is left for its own PR.
  • Sync now nudges one copy of the app. On Vercel it now fetches immediately on that copy, and the others pick the change up within 30 seconds of their next request, not on the click itself.

Docs: AGENT-INSTALL.md updated: where waitUntil is found now includes Vercel's request context, and a failed background check no longer delays the waited refresh. Shipped-doc change, so the hostile field test is owed after the release that carries it.

🤖 Generated with Claude Code

A Next.js middleware on Vercel calls the guard with the request alone, so
the guard found no waitUntil and its 30-second background rules check
was cut off when the response went out. Each cut-off counted as a failed
refresh and pushed back the refresh a request waits for, up to 8 times
the 5-minute interval, so a copy of the app could keep a deleted rule
for about 40 minutes.

- A background check that fails or is cut off now backs off only its own
  deadline; the waited refresh still comes due on time.
- hostWaitUntil also reads Vercel's request context
  (globalThis[Symbol.for('@vercel/request-context')].get().waitUntil),
  the place @vercel/functions reads it, so background checks on Vercel
  finish after the response.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mariojgt

Copy link
Copy Markdown
Contributor Author

/review

@coderbuds

coderbuds Bot commented Oct 28, 2026

Copy link
Copy Markdown

🎯 Elite quality (96%) — Fixes background-check backoff starving stale refreshes and adds Vercel waitUntil support with targeted regression tests.

📦 Small

See how your team is trending →

@mariojgt
mariojgt merged commit e7aa42f into main Oct 28, 2026
23 checks passed
@mariojgt
mariojgt deleted the mariot/vercel-refresh-waituntil branch October 28, 2026 14:12
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