Replace the output-side magic-byte check with a read-time integrity
verification in the shared cache middleware.
The cache document already stores `signature = md5(payload)` at save
time, but the load path never verified it. If the filesystem blob and
the cache document drift — a partial write, disk corruption, or a bad
entry that predates the content-type save guard — the mismatched md5
surfaces here and we treat the hit as a miss, purge, and let the action
regenerate the response.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Root-cause hardening for the cached-error incident. A transient backend
failure on the storage device can surface as a short body rather than a
thrown exception. Imagick is lenient with truncated input and will still
produce a parsable output blob, which then lands in the 180-day cache as
the authoritative answer for that file.
- After `deviceForFiles->read($path)`, compare `strlen($source)` against
`file.sizeActual` (skipped on the logo-fallback path, where `sizeActual`
doesn't describe the source). Mismatch → STORAGE_FILE_NOT_FOUND, which
is not cached and retries cleanly.
- After `image->output()`, verify the leading bytes match the declared
output format (JPEG/PNG/GIF/WebP). Unknown formats (heic/avif) pass
through so additions to storage-outputs don't silently fail.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Remove the `Cache-Control: no-cache` bypass path. The content-type guard
on save (via the `cache.contentType` route label) already prevents new
poisoned entries from being written; an on-demand bypass is not worth
the additional surface area.
Existing bad entries from the incident can still be cleared by bumping
`APP_CACHE_BUSTER` or relying on the async delete worker on file delete.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.