Skip to content

Stop bots from creating a guest customer on every request - #918

Merged
KrzysztofPajak merged 15 commits into
developfrom
feature/lazy-guest-customers
Oct 15, 2026
Merged

KrzysztofPajak merged 15 commits into
developfrom
feature/lazy-guest-customers

Conversation

@KrzysztofPajak

Copy link
Copy Markdown
Member

Type: bugfix

Issue

Every storefront request that no resolver claims ends in WorkContextSetter.CreateCustomerGuest, which inserts a Customer document and sets the guest cookie. Clients that never send the cookie back create a new document on every request. Crawler detection relied on Wangkanai.Detection, which only recognised user agents containing "bot" plus a few named crawlers, so Bytespider, ...Spider, ...crawler and every scraper with a browser user agent created a guest per request. Detected crawlers caused a second problem: CustomerActivityAttribute wrote LastActivityDateUtc, LastIpAddress and LastVisitedPage to the single shared SearchEngine document on each of their requests.

To reproduce on develop: request the home page several times without a cookie jar (curl -A "Mozilla/5.0 (compatible; Bytespider)" http://localhost:<port>/) and count the documents in the Customer collection.

Solution

  • Unsaved guest. A guest created on a GET/HEAD/OPTIONS request without a guest cookie stays in memory (IGuestCustomerPersistence, request scope) and gets its cookie and the Guests group. It is inserted when the browser returns the cookie (promotion in WorkContextSetter), when something writes to it (an ICustomerService decorator registered after the business services saves it before the first write), or immediately on any other HTTP method. A cookie GUID that belongs to any existing customer is never promoted; ids are never derived from the cookie.
  • Unique CustomerGuid_1. Concurrent promotions of one cookie collapse into one document. The installer creates the index unique; a 2.5 migration recreates it unique on MongoDB (duplicates are logged and the index stays non-unique; the lookup index is never left missing).
  • Crawler detection. Wangkanai.Detection is replaced by IUserAgentClassifier (crawler names: bot, crawl, spider, slurp, Bytespider, facebookexternalhit, meta-externalagent; mobile/tablet for the admin tab strip). Requests with an Authorization header are never treated as crawlers.
  • Guest cookie. Resolves only to an active guest that is not registered and not a system account; the cookie must hold a GUID in "D" format.
  • Activity tracking skips system accounts and the unsaved guest; recently viewed products are not recorded for the unsaved guest.
  • rel="nofollow" on the language, currency, tax type and theme selector links.

Breaking changes

  • Wangkanai.Detection is removed: IDetectionService is no longer registered and UseDetection() no longer runs. Plugins that injected IDetectionService must use Grand.Web.Common.UserAgent.IUserAgentClassifier.
  • Constructor changes: SearchEngineCustomerResolver (adds IHttpContextAccessor, takes IUserAgentClassifier), AdminTabStripTagHelper (takes IUserAgentClassifier), ProductController (adds IGuestCustomerPersistence). Code that constructs these directly must be updated.
  • ICustomerService is resolved as a decorator (TransientGuestCustomerService); a host that registers ICustomerService through a factory instead of a type now fails at startup with an explanatory exception.
  • Behaviour: a visitor whose first request is a GET and who never returns is no longer stored, so one-request visitors disappear from the guest list and the online customers report.

Testing

  1. Run the storefront on an installed database and note the number of documents in the Customer collection.
  2. curl -s -o /dev/null -H "Accept: text/html" -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120" http://localhost:<port>/ twice without a cookie jar: both return 200, the count does not change.
  3. curl -s -o /dev/null -A "Mozilla/5.0 (compatible; Bytespider)" http://localhost:<port>/: the count does not change and the SearchEngine document's LastVisitedPage is unchanged.
  4. curl -c jar -b jar -H "Accept: text/html" http://localhost:<port>/ twice: the count grows by one after the second call and the new document's CustomerGuid equals the cookie value.
  5. With a fresh cookie jar, open /?affiliate=<friendly name of an active affiliate> once: one new guest with that AffiliateId.
  6. With a fresh cookie jar, open /changelanguage/<seo code>?returnUrl=/: redirect to / (not /login) and one new guest with the LanguageId user field.
  7. Send the home page with the guest cookie set to an administrator's CustomerGuid: a new guest cookie is issued and the administrator's document is unchanged.
  8. Call a web API endpoint with a valid bearer token and user agent python-requests/2.31.0: the call is authorised as the API customer.
  9. Upgrade a 2.4 MongoDB database: db.Customer.getIndexes() lists CustomerGuid_1 with unique: true.
  10. dotnet test GrandNode.slnx: all projects pass.

Copilot AI balanced review requested due to automatic review settings October 15, 2026 06:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Comment thread src/Modules/Grand.Module.Migration/Migrations/2.5/MigrationCustomerGuidUniqueIndex.cs Dismissed
Comment thread src/Tests/Grand.Web.Common.Tests/Filters/CustomerActivityAttributeTests.cs Dismissed
support added 15 commits October 15, 2026 12:17
…Service

The decorator replaced the registered ICustomerService, which overrides a core
service and broke startup for factory registrations. GET actions that write to
the current customer are marked [PersistGuestCustomer] instead (SetLanguage,
SetCurrency, SetTaxType); AffiliateAttribute saves the guest before it stores
the affiliate.

- the CustomerGuid migration makes the index unique on LiteDB too and logs an
  error when the Mongo index did not end up unique
- a failed guest insert is retried by the next write instead of rethrown
@KrzysztofPajak
KrzysztofPajak force-pushed the feature/lazy-guest-customers branch from 3c9f480 to 9080293 Compare October 15, 2026 10:19
Comment thread src/Tests/Grand.Web.Common.Tests/Filters/PersistGuestCustomerAttributeTests.cs Dismissed
@KrzysztofPajak
KrzysztofPajak merged commit c357f16 into develop Oct 15, 2026
6 checks passed
@KrzysztofPajak
KrzysztofPajak deleted the feature/lazy-guest-customers branch October 15, 2026 14:03
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