spartacus⚿site⚿cart and the “Cart is not anonymous” error after session expiry

Pinestran  > Spartacus >  spartacus⚿site⚿cart and the “Cart is not anonymous” error after session expiry

spartacus⚿site⚿cart and the “Cart is not anonymous” error after session expiry

0 Comments

Summary

After a session expires, the previous user’s active cart id can stay in localStorage (spartacus⚿site⚿cart). When another user logs in, Spartacus tries to merge that cart into the new user’s cart as if it were anonymous, and the backend rejects it with “Cart is not anonymous”.

Fixes made:

  1. A new always-loaded service clears the cart key on every logout.
  2. The auth interceptor now logs the user out when the refresh token is rejected with 400 invalid_grant.
  3. The auth interceptor no longer crashes when the cart key is missing.

1. How the key works

Where it comes from

The key is written by Spartacus, not by project code.

  • MultiCartStatePersistenceService.initSync() (@spartacus/cart base core) calls StatePersistenceService.syncWithStorage with:
    • key: 'cart'
    • context: base site (environment.OCC_BASE_SITE = site, see src/app/spartacus/spartacus-configuration.module.ts)
    • storage: localStorage
  • Key format: spartacus⚿<baseSite>⚿cart, so here it is spartacus⚿site⚿cart.
  • Value: {"active":"<cartId>"}.
  • initSync() runs once, when the cart base feature is initialised.

When it is written

It is rewritten whenever cart.index.Active changes in the NgRx store (distinctUntilKeyChanged('Active')).

TriggerNew active value
CreateCart / CreateCartSuccess with extraData.activenew cart id (guid for anonymous users, code for logged-in users)
LoadCart with cartId === 'current' (login, reloadCart('current'), ASM bind, reorder)'' (temporarily)
LoadCartSuccess with extraData.activeloaded cart id, or '' for a saved cart
SetActiveCartId(id) (on startup, from the stored value)id

When it is reset to {"active":""}

  • RemoveCart or DeleteCartSuccess for the active cart (clear cart dialog, temp cart after merge, cart not found, order placed).
  • ClearCartState.
  • Logout: the clearMultiCartState meta-reducer wipes cart state on AuthActions.LOGOUT.

Spartacus never removes the key. persistToStorage only calls setItem and skips undefined.

Startup read

On init (and on base-site change), syncWithStorage reads the key first and calls onRead. That dispatches ClearCartState, then SetActiveCartId(stored.active || '').

Project code that touches the key

FileUsage
src/app/core/services/select-branch/select-branch-component.service.ts (onSuccess)removeItem after a branch switch
src/app/core/services/auth-interceptor.service.tsreads .active to send currentCartId
src/app/features/checkout/checkout-delivery-address/checkout-delivery-address.component.tsreads the key
src/app/core/services/mini-cart/mini-cart-component.service.tsreads via readStateFromStorage
src/app/core/services/cart-storage-reset.service.ts (new)removes on logout

2. Session expiry handling

Handled in CustomAuthInterceptorService (src/app/core/services/auth-interceptor.service.ts) and CustomAuthHttpHeaderService (src/app/core/services/auth-http-header.service.ts).

ResponseAction
401 with InvalidTokenErrorhandleExpiredAccessToken: refresh the token and retry. If it works, the user stays logged in and the cart key does not change.
500 from authorizationserver/oauth/tokenhandleExpiredRefreshToken
401 invalid_token from the token endpointhandleExpiredRefreshToken
400 invalid_grant on a refresh-token requesthandleExpiredRefreshToken (added in this fix)
No refresh token availablehandleExpiredRefreshToken

handleExpiredRefreshToken() calls authService.coreLogout(), which dispatches AuthActions.Logout, and then redirects to /login.


3. The bug

Steps to reproduce

  1. Log in as Account A.
  2. Add a product to the cart.
  3. Close the browser tab.
  4. The next day, open the site directly by URL. You are redirected to the login page.
  5. In DevTools, spartacus⚿site⚿cart still holds A’s cart id.
  6. Log in as Account B.
  7. You are redirected to the homepage with the error “Cart is not anonymous”.

Root cause

  1. The cart library is lazy-loaded (CART_BASE_FEATURE in src/app/spartacus/features/cart/cart-base-feature.module.ts). MultiCartStatePersistenceService, the only code that writes the key, is inside that lazy chunk.
  2. In step 4, the first API call fails and handleExpiredRefreshToken() logs the user out. If the cart chunk has not loaded yet, nothing reacts to the logout, so A’s cart id stays in localStorage.
  3. In step 6, the cart chunk loads, reads the key and sets A’s cart id as the active cart. The previous user id is now anonymous, so ActiveCartService.loadOrMerge calls mergeToCurrentCart. That sends POST /users/B/carts?oldCartId=<A's cart>, and the backend rejects it because A’s cart is not anonymous.

Related issues found

  • The interceptor did not handle 400 invalid_grant from the token endpoint (errorIsInvalidGrant() existed but was never called). Stock Spartacus logs out in this case. If the backend returns 400 for an expired refresh token, no logout happened at all.
  • JSON.parse(localStorage.getItem('spartacus⚿site⚿cart')).active in the interceptor throws when the key is missing.

4. Changes

New: src/app/core/services/cart-storage-reset.service.ts

Listens for Spartacus’s LogoutEvent, which is part of core and always loaded, and removes spartacus⚿<baseSite>⚿cart for the current base site. It covers session expiry, normal logout and ASM logout. Removing the key is safe: Spartacus treats a missing key the same as {"active":""}.

The key is built in the same format as StatePersistenceService.generateKeyWithContext, which is protected and cannot be called directly.

this.eventService
  .get(LogoutEvent)
  .pipe(
    switchMap(() =>
      this.siteContextParamsService
        .getValues([BASE_SITE_CONTEXT_ID])
        .pipe(take(1))
    )
  )
  .subscribe((context) =>
    this.winRef.localStorage?.removeItem(
      `spartacus⚿${([] as string[]).concat(context).join('⚿')}⚿cart`
    )
  );

Changed: src/app/app.module.ts

Registers the service at startup:

provideAppInitializer(() => inject(CartStorageResetService).initSync()),

Changed: src/app/core/services/auth-interceptor.service.ts

New case 400 in the error handler. The grant_type check stops a wrong password at login (also 400 invalid_grant) from triggering a logout.

case 400:
  if (
    this.errorIsInvalidGrant(errResponse) &amp;&amp;
    request.body?.get?.('grant_type') === 'refresh_token'
  ) {
    this.customAuthHttpHeaderService.handleExpiredRefreshToken();
  }
  break;

The currentCartId param now handles a missing key and sends '', the same value sent when the key held {"active":""}:

const cartState = JSON.parse(localStorage.getItem('spartacus⚿site⚿cart') ?? 'null');
params = params.append('currentCartId', cartState?.active ?? '');


5. Verification

Status: the changed files type-check cleanly. The fix has not been tested in a browser yet.

To test without waiting overnight:

  1. Log in as Account A and add a product to the cart.
  2. In DevTools > Application > Local Storage, edit spartacus⚿⚿auth: make access_token invalid and set refresh_token to a value the server will reject.
  3. Reload the page. You should be redirected to the login page, and spartacus⚿site⚿cart should be gone.
  4. Log in as Account B. B’s own cart should load with no “Cart is not anonymous” error.

Also check which status the token endpoint returns for a rejected refresh token (400 invalid_grant or 500). Both now trigger a logout.

6. Known leftovers

  • auth-interceptor.service.ts still has a strict-tsc error where localStorage.getItem('spartacus⚿⚿auth') (string | null) is passed to a function that needs a string. It was there before these changes and was left as is.
  • The site id site is hardcoded in several reads of the cart key.


Leave a Reply

Your email address will not be published. Required fields are marked *