Addresses review feedback on the prior commit:
- The shared cache middleware should not hardcode `image/*`. Replace the
inline content-type check with a generic `cache.contentType` route
label (a prefix match). Each route that opts into caching now declares
`->label('cache.contentType', 'image/')` alongside `->label('cache', true)`.
- A public `Cache-Control: no-cache` bypass is a DDoS vector on expensive
pipelines (Imagick transforms, Chromium screenshots). Restrict the
bypass to privileged callers — API keys, admin console, and other
server-side contexts — so only trusted traffic can force-miss the cache.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously, any 2xx response with a non-empty payload on a route with
`label('cache', true)` was persisted to the filesystem cache for 180
days, and clients had no per-URL way to invalidate it. If an incident
produced a non-image body on one of these routes (e.g. a truncated
read, a fallback body, or a partial Imagick output) the bad content
was served from cache indefinitely.
- Load hook: honor `Cache-Control: no-cache` / `no-store` on the
request. Skip the cache load, let the action re-run, and let the
existing save path overwrite the poisoned entry. Emits
`X-Appwrite-Cache: bypass` and a telemetry counter.
- Save hook: refuse to cache unless the response `Content-Type` is
`image/*`. Every route currently opting into caching sets an image
content type via `Response::file()`; anything else is an error or
fallback and must not become the authoritative cached answer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A phpspy profile of a production databases worker showed the V20
backwards-compat request filter accounting for ~40% of in-request
samples on `databases.listDocuments` traffic. Two compounding causes:
1. `Request::getParams()` re-ran the entire filter chain on every
invocation. The framework and app call `getParams()` several times
per request (route param binding, `cacheIdentifier()`, action
injection, logging), so V20's recursive schema walk executed N times
with identical inputs.
2. Inside `V20::getRelatedCollectionKeys`, the `databases/$databaseId`
document was fetched at every recursion frame (up to
RELATION_MAX_DEPTH = 3), and sibling relationships pointing at the
same related collection each did their own `getDocument` call.
This commit:
- Memoizes the post-filter params on `Request`. The cache is
invalidated by `addFilter`, `resetFilters`, and `setRoute`. `Request`
is constructed per HTTP request (app/http.php), so the memo is
naturally request-scoped. Helps every request filter version, not
just V20.
- Splits V20's walk into an entry point that resolves the database
namespace once and a pure recursive helper.
- Caches the collection `attributes` array per
`(databaseNamespace, collectionId)` on the filter instance, so shared
related collections collapse to one `getDocument` call. Missing or
errored lookups are cached as `null` to avoid retry storms.
- Updated span logging keys to use camelCase for uniformity across connection and message events.
- Added checks to ensure project and user IDs are only logged if they are not empty, enhancing data integrity.
- Improved error handling and logging structure to maintain consistency in telemetry data.
- Introduced new arrays to capture subscribed channels and passed queries during connection and message events.
- Enhanced span logging to include details about channels and queries for better monitoring and analysis.
- Updated telemetry data structure to reflect the new metrics, improving traceability of realtime interactions.
- Introduced span logging for connection open and close events, capturing metrics such as inbound and outbound bytes, subscription counts, and response codes.
- Enhanced error handling with logging of exceptions during connection lifecycle.
- Updated the structure of the telemetry data to include project and user IDs for better traceability.