Issue Summary
Explain roughly what’s wrong
The KnexKvStore implementation within the Ghost ActivityPub integration handles cache misses and empty store values incorrectly, leading to broken incoming federation (HTTP 401) and server crashes (HTTP 500):
- Cache Miss Bug (
401 Unauthorized):KnexKvStore.get()returnsnullinstead ofundefinedwhen a key does not exist or when an expired key is deleted. Fedify interpretsnullas “entry exists in cache but is unavailable” rather than a cache miss. Consequently, Fedify skips fetching actor public keys over HTTP and rejects incoming federation requests (e.g., Follow activities) with401 Unauthorized. - TypeError Crash (
500 Internal Server Error): InKnexKvStore.get(),Object.hasOwn(row.value, "@@BOOLEAN@@")is called without validating thatrow.valueis non-null and an object. Ifrow.valueisnullorundefined, Node.js throwsTypeError: Cannot convert undefined or null to object.
What did you expect to happen?
KnexKvStore.get()should returnundefinedon cache misses or key deletions so Fedify performs network requests to fetch required keys.KnexKvStore.get()should safely validaterow.valuebefore evaluating object properties.
Steps to Reproduce
- Deploy Ghost with the ActivityPub service enabled using MySQL as the KvStore backend.
- Attempt to follow the Ghost ActivityPub account from a remote Mastodon instance.
- Observe incoming
POSTrequests to/.ghost/activitypub/inbox/indexfailing with401 Unauthorized. - Inspect logs to see
fedify·sig·key: Entry '...' found in cache, but it is unavailable. - In scenarios where
row.valueis empty/null, observe request failure withTypeError: Cannot convert undefined or null to objectin_KnexKvStore.get.
Setup Information
- Ghost Version: 6.59.0
- ActivityPub Service Version: 1.2.7 (
@fedify/fedify 2.3.2) - Node.js Version: v22.23.1
- How did you install Ghost? Self-hosted via Docker Compose
- Host & Operating System: Linux x86_64
- Database Type: MySQL 8.0.44
- Browser & OS Version: N/A (Server-to-server ActivityPub federation bug)
Relevant Log / Error Output
1. Cache miss returning null triggering 401 Unauthorized:
DBG activitypub: KnexKvStore: Get key _fedify,publicKey,[https://mastodon.social/ap/users/117115874267027772#rsa-0a8fa5b2ef1e0232](https://mastodon.social/ap/users/117115874267027772#rsa-0a8fa5b2ef1e0232)
DBG fedify·sig·key: Entry '[https://mastodon.social/ap/users/117115874267027772#rsa-0a8fa5b2ef1e0232](https://mastodon.social/ap/users/117115874267027772#rsa-0a8fa5b2ef1e0232)' found in cache, but it is unavailable.
DBG activitypub: KnexKvStore: Get key _fedify,publicKey,__fetchError,[https://mastodon.social/ap/users/117115874267027772#rsa-0a8fa5b2ef1e0232](https://mastodon.social/ap/users/117115874267027772#rsa-0a8fa5b2ef1e0232)
DBG fedify·sig·key: Entry '[https://mastodon.social/ap/users/117115874267027772#rsa-0a8fa5b2ef1e0232](https://mastodon.social/ap/users/117115874267027772#rsa-0a8fa5b2ef1e0232)' found in cache, but no fetch failure details are available.
WRN fedify·federation·http: 'POST' '/.ghost/activitypub/inbox/index': 401
Crash on missing row.value check triggering 500 Internal Server Error:
ERR fedify·federation·http: An error occurred while serving request 'POST' 'https://chromozone.org/.ghost/activitypub/inbox/index': TypeError: Cannot convert undefined or null to object
at Function.hasOwn ()
at _KnexKvStore.get (file:///opt/activitypub/dist/app.js:11967:16)
at async KvKeyCache.get (file:///opt/activitypub/node_modules/.pnpm/@fedify+fedify@2.3.2/node_modules/@fedify/fedify/dist/middleware-B2rDMvhv.js:1422:22)
Proposed Fix
In src/knex.kvstore.ts (and compiled dist/app.js)
// Return undefined instead of null on cache misses
if (!row) {
return undefined;
}
if (row.expires !== null && row.expires <= new Date()) {
await this.knex(this.table).where(query).del();
return undefined;
}
// Add null & type check before Object.hasOwn
if (row.value && typeof row.value === "object" && Object.hasOwn(row.value, "@@BOOLEAN@@")) {
return row.value["@@BOOLEAN@@"];
}
return row.value;