Skip to content

fix: do not cache card search results served by the SQL fallback - #80

Open
Yutsa wants to merge 1 commit into
Altered-Community:mainfrom
Yutsa:fix/search-fallback-no-cache
Open

Yutsa wants to merge 1 commit into
Altered-Community:mainfrom
Yutsa:fix/search-fallback-no-cache

Conversation

@Yutsa

@Yutsa Yutsa commented Oct 6, 2026

Copy link
Copy Markdown

Why

GET /api/cards?name=aérolithe&locale=fr normally searches card names and effect texts through Meilisearch and returns 22 cards. On 2026-10-06, while Meilisearch was being re-indexed, a user got only the 4 cards whose name contains « aérolithe », and kept getting them for a full hour:

  1. SearchAwareCollectionProvider::fetchIds() / fetchTotal() caught every \Throwable and returned null without logging, so the provider silently fell back to the Doctrine provider, where CardNameFilter only does a LIKE on card names.
  2. Meilisearch was most likely timing out (HTTP client: timeout: 2.0, max_duration: 5.0) during the re-index.
  3. The partial response was served with Cache-Control: public, max-age=3600, s-maxage=3600, so the browser reused it for an hour.

How

  • SearchAwareCollectionProvider logs Meilisearch failures (search and total count) at warning level with the query, the filter, the exception message and the exception.
  • The provider records on the request which backend answered (_search_backend: meilisearch | sql) and flags the response as degraded (_search_degraded) when a name search fell back to SQL because Meilisearch failed. A filter-only search that falls back to SQL is not flagged: SQL applies the same filters, so the result is complete.
  • New SearchBackendResponseSubscriber (kernel.response, priority -10, after API Platform has set the operation's cache headers):
    • sets X-Search-Backend: meilisearch|sql (also added to CORS expose_headers);
    • replaces Cache-Control with no-store, private on degraded responses only.
  • The provider's inner dependency is typed as ProviderInterface (still autowired to CardCollectionProvider) so it can be stubbed in unit tests.
  • Timeouts are unchanged.

Outcomes

  • A search answered by the SQL fallback is no longer cached: the next request retries Meilisearch instead of reusing the 4-card result for an hour.
  • Normal responses keep public, max-age=3600, s-maxage=3600.
  • Failures appear in the logs, and clients can check X-Search-Backend.

Note: in prod, Monolog's main handler is fingers_crossed with action_level: error, so these warnings are only written if an error occurs in the same request. Raising them to error, or adding a handler for this channel, would make them always visible.

Tests

  • tests/Unit/State/SearchAwareCollectionProviderTest.php: Meilisearch success (IDs used, no warning), search failure (warning logged with the query, request flagged degraded), count failure (warning, IDs kept), filter-only failure (not degraded), plain listing.
  • tests/Unit/EventSubscriber/SearchBackendResponseSubscriberTest.php: cache headers unchanged for Meilisearch / non-degraded SQL responses, no-store for degraded ones, sub-requests ignored.
  • tests/Integration/Api/CardApiTest.php: /api/cards stays cacheable; /api/cards?name[fr]=aérolithe with Meilisearch unavailable returns no-store and X-Search-Backend: sql.

Ran locally on PHP 8.4 + PostgreSQL 16 + Redis: Unit 37/37, Integration 26/26.

🤖 Generated with Claude Code

When Meilisearch fails, a full-text name search silently falls back to the
SQL name filter, which ignores effect texts. That partial response was then
cached by browsers for an hour (max-age=3600).

- Log Meilisearch search/count failures at warning level.
- Mark degraded responses on the request and override Cache-Control with
  no-store in a kernel.response subscriber.
- Expose the backend that answered via X-Search-Backend (meilisearch|sql).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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