ActivityPub Inbox Processing#
Overview#
InboxProcessorService is the BullMQ job processor for Misskey's ActivityPub inbox queue. It runs as Stage 2 of a two-stage pipeline: Stage 1 (ActivityPubServerService.inbox()) validates the HTTP request and enqueues the activity; Stage 2 performs cryptographic verification and dispatches the activity for processing .
The inbox queue runs at up to 16 concurrent workers (configurable via inboxJobConcurrency) with a 32/sec rate limit, using exponential backoff with a 1-minute base and 8-hour maximum .
Processing Lifecycle#
The process() method executes the following stages in order:
1. Pre-flight Checks #
- Extracts the hostname from
signature.keyId(punycoded) and rejects blocked/non-allowed federation hosts . - Rejects legacy
acct:keyId format . - For
Deleteactivities targeting an actor, checks whether the actor exists locally; if not, skips silently to avoid unnecessary processing .
2. Remote Actor Resolution #
Actor resolution proceeds in two steps:
-
Key-first lookup — calls
getAuthUserFromKeyId(signature.keyId), which queries theUserPublickeytable by keyId (with a 12-hour cache). This works for actors already known to the local instance . -
Actor URI fallback — if the keyId lookup returns
null, callsgetAuthUserFromApId(activity.actor), which resolves the actor URI viaApPersonService.resolvePerson()(will fetch remotely if needed) and retrieves the public key from a separate cache .
If either resolution raises a StatusError, error classification applies (see below). If resolution still fails, the job is permanently discarded via Bull.UnrecoverableError .
Note: PR #13470 (merged) proposed passing
signature.keyIdas a hint togetAuthUserFromApId()to improve key selection for actors with multiple keys (e.g., EdDSAadditionalKeys). That PR's summary indicates the two-step lookup was collapsed into a singlegetAuthUserFromApId(actor, keyId)call; the snapshot above reflects an earlier version of the file.
3. HTTP Signature Verification #
- Verifies the signature via
httpSignature.verifySignature(signature, authUser.key.keyPem). - Also asserts
authUser.user.uri === activity.actor— the signing key owner must match the declared actor . - If HTTP Signature fails but an embedded
activity.signatureobject exists, falls back to LD-Signature (RsaSignature2017); see below . - If HTTP Signature fails and no LD-Signature exists →
Bull.UnrecoverableError.
4. LD-Signature Fallback #
Only RsaSignature2017 is supported; other types are immediately discarded .
Flow:
- Strips
activity.signature, JSON-LD compacts the activity, checks for forbidden directives . - Resolves the LD signer via
getAuthUserFromKeyId(ldSignature.creator). - Verifies via
JsonLdService.verifyRsaSignature2017(). - Re-asserts
authUser.user.uri === activity.actorand re-checks federation allowlist for the LD signer's host .
Bug fix (PR #17615): Prior to this fix, non-JsonLdError exceptions thrown inside verifyRsaSignature2017 would escape the catch block unwrapped, causing Bull to spuriously retry the job and emit error-level stack traces. The fix wraps all non-UnrecoverableError exceptions from that block into Bull.UnrecoverableError .
5. Activity ID Host Validation #
After signature is confirmed, asserts that activity.id is a string whose hostname matches the verified signer's host. Mismatches → Bull.UnrecoverableError.
6. Activity Dispatch #
Calls ApInboxService.performActivity(authUser.user, activity) .
The catch block around this call swallows several IdentifiableError cases — returning a string instead of throwing — to prevent unnecessary retries or failures for semantically valid but locally-skippable conditions:
| Error ID | Swallowed reason |
|---|---|
689ee33f-... | Note contains prohibited words |
85ab9bd7-... | Actor has been suspended |
d450b8a9-... | Invalid Note |
9f466dab-... | Note contains too many mentions |
09d79f9e-... | Instance is blocked |
All other exceptions propagate and trigger Bull's retry machinery .
7. Instance Stat Updates #
Run asynchronously via process.nextTick(): updates latestRequestReceivedAt, unsuspends auto-suspended instances, and triggers instance metadata refresh. Uses a CollapsedQueue to batch updates (5-minute window in production) and avoid thundering-herd writes .
Error Classification: StatusError vs Bull.UnrecoverableError#
StatusError (packages/backend/src/misc/status-error.ts) wraps HTTP-layer errors from remote fetches:
isClientError:truefor 4xx status codes.isRetryable:truewhen NOT a client error, or when the status is 429 (Too Many Requests).
Decision table for getAuthUserFromApId() errors in the inbox :
StatusError.isRetryable | Action |
|---|---|
false (4xx, not 429) | Throw Bull.UnrecoverableError → job permanently discarded |
true (5xx or 429) | Re-throw as plain Error → Bull retries with exponential backoff |
HTTP 429 was explicitly made retryable in PR #12917 to respect remote rate limits rather than permanently dropping activities.
Key Files#
| File | Role |
|---|---|
InboxProcessorService.ts | Main job processor (this article) |
ApDbResolverService.ts | getAuthUserFromKeyId() / getAuthUserFromApId() |
StatusError.ts | HTTP error wrapper with retry classification |
ApInboxService.ts | Activity type dispatch (Create, Delete, Follow, etc.) |
JsonLdService.ts | LD-Signature verification |
ActivityPubServerService.ts | Stage 1: HTTP validation and enqueue |
QueueProcessorService.ts | Queue configuration and backoff strategy |