Skip to content

Commit bf8390a

Browse files
fix: wait for hCaptcha widget before executing token request
getCaptchaToken() was returning null immediately when captchaWidgetId === null (hCaptcha script still loading), causing the server to reject with 403 for any user who submitted a message before the widget finished rendering. Fix: introduce captchaWidgetReady Promise that resolves once hcaptcha.render() completes. getCaptchaToken() now chains onto it so early sends wait (up to the 15s timeout) rather than sending a null token.
1 parent d06b440 commit bf8390a

1 file changed

Lines changed: 14 additions & 4 deletions

File tree

EssentialCSharp.Web/wwwroot/js/chat-module.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ let captchaTokenReject = null;
2121
let captchaPending = false; // prevents concurrent token requests overwriting promise callbacks
2222
const CAPTCHA_TIMEOUT_MS = 15_000;
2323

24+
// Resolves once the widget has rendered. getCaptchaToken() awaits this so a user who
25+
// submits before hCaptcha finishes loading waits (up to 15 s) rather than getting a 403.
26+
let captchaWidgetReadyResolve = null;
27+
const captchaWidgetReady = captchaSiteKey
28+
? new Promise((resolve) => { captchaWidgetReadyResolve = resolve; })
29+
: Promise.resolve();
30+
2431
function initCaptchaWidget() {
2532
if (!captchaSiteKey) return;
2633
// Guard: only render once (widget lives outside the v-if dialog overlay)
@@ -52,23 +59,26 @@ function initCaptchaWidget() {
5259
reject?.(new Error('captcha-error'));
5360
}
5461
});
62+
captchaWidgetReadyResolve?.(); // unblock any getCaptchaToken() calls waiting for the widget
5563
});
5664
}
5765

5866
/**
5967
* Returns a fresh hCaptcha token, or null if captcha is not configured.
60-
* Resolves after the invisible challenge completes (typically instant for non-suspicious users).
68+
* Waits for the widget to finish rendering if it has not yet (handles slow script loads).
6169
* Rejects with 'captcha-concurrent' if a token request is already in-flight.
6270
* Rejects with 'captcha-timeout' if the widget does not respond within 15 seconds.
6371
*/
6472
function getCaptchaToken() {
65-
if (!captchaSiteKey || captchaWidgetId === null) return Promise.resolve(null);
73+
if (!captchaSiteKey) return Promise.resolve(null);
6674
if (captchaPending) return Promise.reject(new Error('captcha-concurrent'));
6775
captchaPending = true;
6876

6977
let timeoutId;
7078

71-
const tokenPromise = new Promise((resolve, reject) => {
79+
// Chain onto captchaWidgetReady so calls made before the widget finishes loading
80+
// wait rather than immediately returning null and causing a 403.
81+
const tokenPromise = captchaWidgetReady.then(() => new Promise((resolve, reject) => {
7282
captchaTokenResolve = (token) => {
7383
captchaPending = false;
7484
clearTimeout(timeoutId); // cancel lingering timer so it can't corrupt the next call
@@ -80,7 +90,7 @@ function getCaptchaToken() {
8090
reject(err);
8191
};
8292
window.hcaptcha.execute(captchaWidgetId);
83-
});
93+
}));
8494

8595
const timeoutPromise = new Promise((_, reject) => {
8696
// timeoutId is assigned synchronously here, before captchaTokenResolve can fire

0 commit comments

Comments
 (0)