antiwork/gumroad · error · RecaptchaUnavailableError
We couldn't load the security check. This is often caused…
Error message
We couldn't load the security check. This is often caused by an ad blocker or privacy extension — try disabling it for this page, using a private/incognito window, or switching networks, then try again.
What it means
RecaptchaUnavailableError thrown from useRecaptcha's score-based (invisible) branch when the Google reCAPTCHA Enterprise script cannot be loaded — loadRecaptchaScript(scoreScriptUrl(siteKey)) rejects — or when grecaptcha.enterprise.execute() fails to produce a token. The class itself carries no message; callers (Checkout PaymentForm, follow forms) render the shared RECAPTCHA_UNAVAILABLE_MESSAGE guidance when they catch it. It is deliberately distinct from RecaptchaCancelledError (user dismissed a challenge): this error is environmental — an extension or network is blocking Google.
Solutions
- Disable the ad blocker/privacy extension for the page or use a private/incognito window — exactly what the message suggests.
- Verify the page CSP allows script-src https://www.google.com (plus frame-src/connect-src for the widget flow).
- Confirm the reCAPTCHA Enterprise site key's allowed domains include the current domain.
- Test from another network to rule out filtered connectivity to google.com.
Defensive patterns
Strategy: fallback
Type guard
import { RecaptchaCancelledError, RecaptchaUnavailableError } from "$app/components/useRecaptcha";
const isRecaptchaUnavailable = (e: unknown): e is RecaptchaUnavailableError => e instanceof RecaptchaUnavailableError;
const isRecaptchaCancelled = (e: unknown): e is RecaptchaCancelledError => e instanceof RecaptchaCancelledError; Try / catch
try {
const token = await execute();
} catch (e) {
if (isRecaptchaCancelled(e)) return; // user dismissed; retry quietly
if (isRecaptchaUnavailable(e)) {
showAlert(RECAPTCHA_UNAVAILABLE_MESSAGE, "error"); // actionable guidance
return;
}
throw e;
} Prevention
- Include www.google.com in script-src/frame-src CSP entries wherever the hook is used.
- Start loading the reCAPTCHA script on page mount, not on submit, so load failures surface earlier.
- Whitelist staging domains on the reCAPTCHA Enterprise key.
- Preserve the Cancelled vs Unavailable distinction — never show blocker guidance for a user dismissal.
When it happens
Trigger: Loading https://www.google.com/recaptcha/enterprise.js?render=<siteKey> fails: an ad blocker/privacy extension blocks it, CSP script-src doesn't allow www.google.com, the network is offline or filters Google (e.g. mainland China), or enterprise.execute() rejects (site key not valid for the domain).
Common situations: Checkout or follow-button submissions failing for a subset of buyers running uBlock/Privacy Badger; local dev with restrictive CSP; staging domains not added to the reCAPTCHA key's allowed-domain list; corporate proxies blocking third-party scripts.
Related errors
- # is no longer available in the quantity this bundle…
- error_message (preorder.errors.full_messages[0])
- Gift purchases cannot be on installment plans.
- giftee_purchase.errors.full_messages[0]
- Gifting is not yet enabled for pre-orders.
AI-assisted analysis of antiwork/gumroad@afeacbd394 (2026-08-21).
Data as JSON: /api/errors/44af4e64229ee85a.
Report an issue: GitHub.
Appendix: source
Thrown at app/javascript/components/useRecaptcha.tsx:145
});
});
const initPromise = loadRecaptchaScript(CHALLENGE_SCRIPT_URL).then(initRecaptcha);
initPromiseRef.current = initPromise;
// Swallow the rejection here so a blocked script doesn't surface as an unhandled promise
// rejection — execute() re-checks the stored promise and reports the failure to the user.
initPromise.catch(() => {});
}, [siteKey, scoreBased]);
const execute = () => {
if (!siteKey) return Promise.reject(new RecaptchaCancelledError());
if (scoreBased) {
return loadRecaptchaScript(scoreScriptUrl(siteKey))
.catch(() => {
// The script failing to load is environmental (blocked by an extension or the
// network), not a user dismissal — surface it as such so the UI can show guidance.
throw new RecaptchaUnavailableError();
})
.then(
() =>
new Promise<string>((resolve, reject) => {
grecaptcha.enterprise.ready(() => {
grecaptcha.enterprise.execute(siteKey, { action }).then(resolve, () => {
// Score keys never show a challenge, so there is nothing for the user to
// dismiss — a token failure here is also environmental.
reject(new RecaptchaUnavailableError());
});
});
}),
);
}
// Wait for initialization to finish before checking the widget: a submit that lands
// before the script has loaded and rendered the widget is a normal timing race, not a
// blocked CAPTCHA, so we must not report it as unavailable prematurely.View on GitHub (pinned to afeacbd394)