Skip to content

Give each widget on a page its own frictionless session - #3620

Merged
HughParry merged 3 commits into
mainfrom
fix/per-widget-frictionless-dedup
Oct 26, 2026
Merged

HughParry merged 3 commits into
mainfrom
fix/per-widget-frictionless-dedup

Conversation

@HughParry

Copy link
Copy Markdown
Contributor

What changed

When a page has several Procaptcha widgets, every widget after the first used to fail its first try with No session found (a 400 on /captcha/pow).

Why: the provider gives the same live session to every /frictionless call from one user, IP and site key. That is the "dedup" that stops a reload from minting a new session. So all the widgets on the page got the same session id. The first widget to be clicked used it up, and every other widget then got the 400 and had to fetch a fresh session and try again. That cost each extra widget one more frictionless call and one more PoW call. If several were clicked at once, a widget could run out of retries and reset to unchecked (7 of 8 passed).

Fix: each widget now tells the provider which widget it is: slot 0, 1, 2… in page order, counted per site key and capped at 15. The provider adds the slot to the dedup key, so each widget gets its own session.

  • Slot 0 uses exactly the old key, so single-widget pages behave the same as before.
  • A reload asks for the same slots, so each widget still reuses its own session and nothing new is minted.
  • Widgets past the cap share slot 15, which is the old behaviour (they still recover via the existing retry).

Why this approach

  • Don't reuse a session that was handed to another widget: the provider can't tell "a second widget on the page" from "the same widget after a reload". So this would either turn off reload dedup or need a widget id anyway.
  • Let the client handle the 400 more cheaply: the session really is used up, so the widget still needs a new one. That only makes the failure cheaper, and the simultaneous-click case keeps racing.
  • Widget-aware key (this PR): the slot is sent by the client, so a client can hold up to 16 live sessions instead of 1. That doesn't weaken anything in practice: the user value is already client-generated, so a bot can mint as many sessions as it likes by changing it. Dedup's real job, keeping a reload on the same session, is unchanged.

One side effect: userSitekeyIpHash is different for widgets in slot 1 and up, so a routing-bucket rule that matches a specific userSitekeyIpHashIn value only matches the first widget on a multi-widget page.

Testing

Unit tests:

  • widgetSlot.test.ts covers the lowest-free slot, per-site-key counting, the cap, and release.
  • procaptchaFrictionless.test.ts: two mounted widgets send slots 0 and 1, and a remount reuses the freed slot.
  • getFrictionlessCaptchaChallenge.unit.test.ts: each slot looks up its own dedup session, and slot 0 matches no slot. A slot past the cap is rejected. This test fails if the handler drops the slot.
  • hashUserIp.unit.test.ts: slot 0 keeps the original hash and later slots each get their own.

Manual check, on a local stack with a page of 8 widgets for one site key:

before after
Click one at a time 8/8 pass, 5× No session found plus extra frictionless/PoW calls 8/8 pass, 0× No session found, exactly 8 frictionless and 8 PoW calls
Click all 8 within ~50ms 7/8 (from the original report) 8/8, 0× No session found
Reload without clicking — 8× reuse_session across 8 different sessions

The provider dedups /frictionless by user + IP + site key, so every widget
on a page was handed the same live session. The first challenge used it
up and every later widget got No session found and had to re-mint.

Each widget now sends a slot (lowest free index per site key, capped at
15) and the provider folds it into the dedup key. Slot 0 keeps the old
key, and a reload still reuses each widget's own session.
@HughParry
HughParry enabled auto-merge (squash) October 26, 2026 13:41
…onless-dedup

# Conflicts:
#	packages/api/src/api/ProviderApi.ts
#	packages/procaptcha-frictionless/src/customDetectBot.ts
#	packages/provider/src/api/captcha/getFrictionlessCaptchaChallenge/handler.ts
#	packages/types/src/provider/api.ts
@HughParry
HughParry merged commit d6cc6fe into main Oct 26, 2026
20 checks passed
@HughParry
HughParry deleted the fix/per-widget-frictionless-dedup branch October 26, 2026 14:15
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