Skip to content

fix(api-cache): list cache keys cover the request path and session scope - #300

Merged
roncodes merged 1 commit into
release/v1.6.69from
fix/api-model-cache-request-scope
Oct 12, 2026
Merged

roncodes merged 1 commit into
release/v1.6.69from
fix/api-model-cache-request-scope

Conversation

@roncodes

Copy link
Copy Markdown
Member

What

The list (query) cache key was built from the table, the session company, the query string and has_callback. Two groups of requests that return different data produced the same key:

  • Stores and networks of the same company. Storefront's SetStorefrontSession resolves the store or network key into session('storefront_store') / session('storefront_network'). Storefront's public list endpoints filter on those values inside the cached callback, for example Product::queryWithRequestCached(... if (session('storefront_store')) $query->where('store_uuid', ...)). The value is not in the query string, so store A's products, catalogs, food trucks, reviews or categories were served to store B, or to a network of the same company, when both sent the same query.
  • Internal and public endpoints. For example, storefront/int/v1/catalogs (callback onQueryRecord with the editor eager loads; internal requests return a fastPaginate paginator) and storefront/v1/catalogs (store filter; returns a collection) both send has_callback=true. The same company plus the same query gave the same key, so the console's paginator, which holds every catalog in the company, could be served to the public API, or the public collection to the console. The same applies to any core-api resource whose internal and public lists both pass a callback, or both pass none.

Change

ApiModelCache::generateQueryCacheKey() now hashes three things together instead of the params alone:

  • params: the query string plus additional params, unchanged.
  • path: $request->path(). This separates internal from public endpoints, one endpoint from another, and endpoints whose callbacks read route parameters (/v1/customers/{id}/places).
  • scope: the values of the session keys listed in the new api.cache.scope.session config. The default is api_credential, storefront_store and storefront_network. Missing values are left out, so console requests are unaffected. Extensions add any session key that a query callback reads but that does not appear in the query string.

The key's visible shape is unchanged: {api_query}:{table}:company_{uuid}:v{n}:{hash}. This builds on #299: the version segment and invalidation are untouched. Path and scope are inside the hash, so API and Storefront keys never appear in the key or in the X-Cache-Key debug header.

When this deploys, every list key changes once (one miss per key), and the old entries expire after their 5-minute TTL.

Tests

tests/Unit/Support/ApiModelCacheTest.php adds four tests. All four failed against the previous code:

  • Two stores and a network of one company with the same query each get their own cached list; a repeat request by store A is a HIT on A's list. Before the fix, store B received store a product.
  • An internal list cached by the console is not served to the public endpoint; a repeat console request is still a HIT. Before the fix, the public request received the console's cached value.
  • Public endpoints that differ only by route parameter get different keys.
  • Only the configured session keys scope the key.

Validation

  • vendor/bin/pest tests/Unit/Support/ApiModelCacheTest.php tests/Unit/Traits/HasApiModelBehaviorTest.php: 36 passed (297 assertions). Before the fix: 4 failed.
  • php -d memory_limit=512M vendor/bin/pest (full suite): 2063 passed, 0 failed (12766 assertions). The deprecations come from cknow/laravel-money under PHP 8.4.
  • php-cs-fixer fix -v --dry-run: 0 of 837 files to fix.

Docs / API impact

  • API behavior: no endpoint, payload, schema or auth changes. Responses are now correctly scoped. No fleetbase/postman update is needed.
  • Docs: no fleetbase/fleetbase.io update is needed. The docs and specs don't mention the API cache (API_CACHE_*, X-Cache-Key or cache keys). The new api.cache.scope.session option is a config array with no env variable, and it is documented in config/api.php.

Related: fleetbase/storefront, a separate PR for NetworkController::addStores, found in the same audit.

List cache keys were built from the table, the session company, the query
string and has_callback only. Query callbacks that filter on session values
(storefront_store, storefront_network) or on route parameters, and internal
and public endpoints that paginate and eager load differently, therefore
shared one cache entry per company and query: one store's products,
catalogs, reviews or categories were served to another store or network of
the same company, and an internal console paginator could be served to the
public API (or the reverse).

The params hash now also covers the request path and the values of the
session keys listed in api.cache.scope.session (default: api_credential,
storefront_store, storefront_network). Both are hashed, so credentials do
not appear in the key or the X-Cache-Key debug header.
@roncodes roncodes added the bug Something isn't working label Oct 12, 2026
@roncodes
roncodes merged commit 5162fe1 into release/v1.6.69 Oct 12, 2026
2 of 3 checks passed
@roncodes
roncodes deleted the fix/api-model-cache-request-scope branch October 12, 2026 14:56
roncodes added a commit that referenced this pull request Oct 12, 2026
Keeps #300's SESSION_SCOPE alongside the dependency memo, and the list
key now combines #300's path and session scoped params hash with the
dependency version segment. #300's store scoping test expects the
v{table-wide}.{company} list version this branch introduced.
roncodes added a commit that referenced this pull request Oct 13, 2026
getSessionScope returns no scope when the request has no session. Every test
request had one, so the release branch fell one statement short of the coverage
floor after #300.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant