Having an AuthorizePredicate on an actor dispatcher breaks interoperability with GoToSocial
Summary
As described in Enabling authorized fetch, it is possible to add an AuthorizePredicate to an actor dispatcher to allow or deny access to actors based on the requester's identity (as authenticated by the signature key).
Using Fedify 1.9.0, if I attempt to fetch such an actor from a remote instance of GoToSocial, Fedify in turn fetches first the key used to sign the incoming request, then the actor which owns it. Fedify does not offer a way to sign these requests, but GoToSocial requires fetches of actors to be signed. Fedify encounters an HTTP 401 error that it does not handle, raising an uncaught FetchError.
From what I can tell, having any AuthorizePredicate, even a trivial one, breaks all interaction with GoToSocial.
Expected Behavior
It should be possible to have an AuthorizePredicate on an actor dispatcher and still allow ActivityPub interoperability with GoToSocial and other servers that require actor fetches to be signed.
Actual Behavior
If any AuthorizePredicate is present, Fedify attempts to make an unsigned fetch of the actor that owns the key, which, if it is denied, causes an error.
We can see the code that causes this behavior starting in line 109 of federation/handler.ts. In particular, the key is fetched in line 110 and the key owner in line 120. Both fetches offer no way to make sure they are signed.
Solving this issue may require a new way to declare for an actor dispatcher that these kinds of follow-up requests of remote data should use a custom DocumentLoader.
It is worth noting that this issue surfaces an interoperability issue as well as a bug in the internal error management. For interoperability, it should be possible to ensure that requests for remote actors are signed. Independently of that, an authentication error (HTTP 401) on a remote actor should not crash the actor handler regardless of how it arises.
Environment
Fedify 1.9.0 running on Deno 2.5.4, up-to-date Ubuntu Linux 24.04
Logs / Screenshots
Fedify log at trace level, showing an attempt from a remote GoToSocial 0.20.1 server (set up for this test) to access an actor:
21:25:01.050 DBG fedify·sig·key Fetching key "https://gts.jfietkau.me/users/julian/main-key" to verify signature...
21:25:01.052 DBG fedify·runtime·docloader Fetching document: "GET" "https://gts.jfietkau.me/users/julian/main-key" {
accept: "application/activity+json, application/ld+json",
"user-agent": "Encyclia/0.0.1 (Fedify/1.9.0; Deno/2.5.4; +https://encyclia.pub)"
}
21:25:01.075 DBG fedify·runtime·docloader Fetched document: 200 "https://gts.jfietkau.me/users/julian/main-key" {
"cache-control": "public, max-age=604800",
"content-security-policy": "default-src 'self'; connect-src 'self' https://api.listenbrainz.org/1/user/; object-src 'none'; img-src 'self' blob:; media-src 'self'",
"content-type": "application/activity+json",
date: "Tue, 21 Oct 2025 21:25:01 GMT",
"permissions-policy": "browsing-topics=()",
"referrer-policy": "same-origin",
server: "gotosocial",
vary: "Accept,Accept-Encoding",
"x-content-type-options": "nosniff",
"x-frame-options": "DENY",
"x-ratelimit-limit": "300",
"x-ratelimit-remaining": "299",
"x-ratelimit-reset": "2025-10-21T21:30:01.000Z",
"x-request-id": "000036g8n12dg000008g",
"x-robots-tag": "noindex, nofollow, noai, noimageai"
}
21:25:01.083 DBG fedify·runtime·docloader Using preloaded context: "https://w3id.org/security/v1".
21:25:01.084 DBG fedify·runtime·docloader Using preloaded context: "https://www.w3.org/ns/activitystreams".
21:25:01.085 DBG fedify·runtime·docloader Using preloaded context: "https://w3id.org/security/v1".
21:25:01.085 DBG fedify·runtime·docloader Using preloaded context: "https://www.w3.org/ns/activitystreams".
21:25:01.096 DBG fedify·runtime·docloader Fetching document: "GET" "https://gts.jfietkau.me/users/julian" {
accept: "application/activity+json, application/ld+json",
"user-agent": "Encyclia/0.0.1 (Fedify/1.9.0; Deno/2.5.4; +https://encyclia.pub)"
}
21:25:01.103 ERR fedify·runtime·docloader Failed to fetch document: 401 "https://gts.jfietkau.me/users/julian" {
"cache-control": "no-store",
"content-security-policy": "default-src 'self'; connect-src 'self' https://api.listenbrainz.org/1/user/; object-src 'none'; img-src 'self' blob:; media-src 'self'",
"content-type": "application/json",
date: "Tue, 21 Oct 2025 21:25:01 GMT",
"permissions-policy": "browsing-topics=()",
"referrer-policy": "same-origin",
server: "gotosocial",
vary: "Accept-Encoding",
"x-content-type-options": "nosniff",
"x-frame-options": "DENY",
"x-ratelimit-limit": "300",
"x-ratelimit-remaining": "298",
"x-ratelimit-reset": "2025-10-21T21:30:01.000Z",
"x-request-id": "000036g8n12dg0000090",
"x-robots-tag": "noindex, nofollow, noai, noimageai"
}
21:25:01.104 ERR fedify·federation·http An error occurred while serving request "GET" "https://encyclia.pub/0000-0001-7264-8496": FetchError: https://gts.jfietkau.me/users/julian: HTTP 401: https://gts.jfietkau.me/users/julian
at getRemoteDocument (https://jsr.io/@fedify/fedify/1.9.0/src/runtime/docloader.ts:198:11)
at load (https://jsr.io/@fedify/fedify/1.9.0/src/runtime/docloader.ts:373:12)
at eventLoopTick (ext:core/01_core.js:179:7)
at async https://jsr.io/@fedify/fedify/1.9.0/src/runtime/docloader.ts:520:25
at async https://jsr.io/@fedify/fedify/1.9.0/src/vocab/vocab.ts:20718:25
at async CryptographicKey.#fetchOwner (https://jsr.io/@fedify/fedify/1.9.0/src/vocab/vocab.ts:20713:12)
at async CryptographicKey.getOwner (https://jsr.io/@fedify/fedify/1.9.0/src/vocab/vocab.ts:20911:23)
at async getKeyOwner (https://jsr.io/@fedify/fedify/1.9.0/src/sig/owner.ts:136:13)
at async RequestContextImpl.getSignedKeyOwner (https://jsr.io/@fedify/fedify/1.9.0/src/federation/middleware.ts:2692:35)
at async handleActor (https://jsr.io/@fedify/fedify/1.9.0/src/federation/handler.ts:120:20) {
url: URL {
href: "https://gts.jfietkau.me/users/julian",
origin: "https://gts.jfietkau.me",
protocol: "https:",
username: "",
password: "",
host: "gts.jfietkau.me",
hostname: "gts.jfietkau.me",
port: "",
pathname: "/users/julian",
hash: "",
search: ""
},
name: "FetchError"
}
An error occurred during route handling or page rendering.
FetchError: https://gts.jfietkau.me/users/julian: HTTP 401: https://gts.jfietkau.me/users/julian
at getRemoteDocument (https://jsr.io/@fedify/fedify/1.9.0/src/runtime/docloader.ts:198:11)
at load (https://jsr.io/@fedify/fedify/1.9.0/src/runtime/docloader.ts:373:12)
at eventLoopTick (ext:core/01_core.js:179:7)
at async https://jsr.io/@fedify/fedify/1.9.0/src/runtime/docloader.ts:520:25
at async https://jsr.io/@fedify/fedify/1.9.0/src/vocab/vocab.ts:20718:25
at async CryptographicKey.#fetchOwner (https://jsr.io/@fedify/fedify/1.9.0/src/vocab/vocab.ts:20713:12)
at async CryptographicKey.getOwner (https://jsr.io/@fedify/fedify/1.9.0/src/vocab/vocab.ts:20911:23)
at async getKeyOwner (https://jsr.io/@fedify/fedify/1.9.0/src/sig/owner.ts:136:13)
at async RequestContextImpl.getSignedKeyOwner (https://jsr.io/@fedify/fedify/1.9.0/src/federation/middleware.ts:2692:35)
at async handleActor (https://jsr.io/@fedify/fedify/1.9.0/src/federation/handler.ts:120:20) {
url: URL {
href: "https://gts.jfietkau.me/users/julian",
origin: "https://gts.jfietkau.me",
protocol: "https:",
username: "",
password: "",
host: "gts.jfietkau.me",
hostname: "gts.jfietkau.me",
port: "",
pathname: "/users/julian",
hash: "",
search: ""
},
name: "FetchError"
}
The same event logged by the GoToSocial server:
timestamp="21/10/2025 21:25:00.783" func=server.Start.Logger.func11.1 level=INFO latency="18.042µs" userAgent="Mozilla/5.0 (X11; Linux x86_64; rv:144.0) Gecko/20100101 Firefox/144.0" method=OPTIONS statusCode=204 uri=/api/v2/search?q=https:%2F%2Fencyclia.pub%2F0000-0001-7264-8496&resolve=true&limit=11 clientIP=2003:c5:8701:f175:79af:5e85:b498:ebf8 requestID=000036g8n12dg000007g msg="No Content: wrote 0B"
timestamp="21/10/2025 21:25:00.805" func=api.(*Client).Route.TokenCheck.func1 level=TRACE requestID=000036g8n12dg0000080 msg="authenticated user 01M045AR8F83P0YPBN6EGQG8V2 with bearer token, scope is read write follow push"
timestamp="21/10/2025 21:25:00.806" func=api.(*Client).Route.TokenCheck.func1 level=TRACE requestID=000036g8n12dg0000080 msg="authenticated client 01K4PGR4CSZ4G7DVXJS2ARNKW6 with bearer token, scope is read write follow push"
timestamp="21/10/2025 21:25:00.806" func=search.(*Processor).Get level=DEBUG maxID="" minID="" limit=11 offset=0 query=https://encyclia.pub/0000-0001-7264-8496 queryType="" resolve=true following=false fromAccountID="" requestID=000036g8n12dg0000080 msg="beginning search"
timestamp="21/10/2025 21:25:00.807" func=bundb.queryHook.AfterQuery level=TRACE duration="601.077µs" query="SELECT \"domain\" FROM \"domain_allows\"" requestID=000036g8n12dg0000080
timestamp="21/10/2025 21:25:00.807" func=bundb.queryHook.AfterQuery level=TRACE duration="470.848µs" query="SELECT \"domain\" FROM \"domain_blocks\"" requestID=000036g8n12dg0000080
timestamp="21/10/2025 21:25:00.809" func=bundb.queryHook.AfterQuery level=TRACE duration=1.022737ms query="SELECT \"account\".\"id\", \"account\".\"created_at\", \"account\".\"updated_at\", \"account\".\"fetched_at\", \"account\".\"username\", \"account\".\"domain\", \"account\".\"avatar_media_attachment_id\", \"account\".\"avatar_remote_url\", \"account\".\"header_media_attachment_id\", \"account\".\"header_remote_url\", \"account\".\"display_name\", \"account\".\"emojis\", \"account\".\"fields\", \"account\".\"fields_raw\", \"account\".\"note\", \"account\".\"note_raw\", \"account\".\"also_known_as_uris\", \"account\".\"moved_to_uri\", \"account\".\"move_id\", \"account\".\"locked\", \"account\".\"discoverable\", \"account\".\"uri\", \"account\".\"url\", \"account\".\"inbox_uri\", \"account\".\"shared_inbox_uri\", \"account\".\"outbox_uri\", \"account\".\"following_uri\", \"account\".\"followers_uri\", \"account\".\"featured_collection_uri\", \"account\".\"actor_type\", \"account\".\"private_key\", \"account\".\"public_key\", \"account\".\"public_key_uri\", \"account\".\"public_key_expires_at\", \"account\".\"memorialized_at\", \"account\".\"sensitized_at\", \"account\".\"silenced_at\", \"account\".\"suspended_at\", \"account\".\"suspension_origin\", \"account\".\"hides_to_public_from_unauthed_web\", \"account\".\"hides_cc_public_from_unauthed_web\" FROM \"accounts\" AS \"account\" WHERE (\"account\".\"uri\" = 'https://encyclia.pub/0000-0001-7264-8496')" requestID=000036g8n12dg0000080
timestamp="21/10/2025 21:25:00.809" func=bundb.queryHook.AfterQuery level=TRACE duration="354.568µs" query="SELECT \"account\".\"id\" FROM \"accounts\" AS \"account\" WHERE (\"account\".\"url\" = 'https://encyclia.pub/0000-0001-7264-8496')" requestID=000036g8n12dg0000080
timestamp="21/10/2025 21:25:00.810" func=bundb.queryHook.AfterQuery level=TRACE duration="658.24µs" query="SELECT \"account_settings\".\"account_id\", \"account_settings\".\"created_at\", \"account_settings\".\"updated_at\", \"account_settings\".\"privacy\", \"account_settings\".\"sensitive\", \"account_settings\".\"language\", \"account_settings\".\"status_content_type\", \"account_settings\".\"theme\", \"account_settings\".\"custom_css\", \"account_settings\".\"enable_rss\", \"account_settings\".\"hide_collections\", \"account_settings\".\"web_layout\", \"account_settings\".\"interaction_policy_direct\", \"account_settings\".\"interaction_policy_mutuals_only\", \"account_settings\".\"interaction_policy_followers_only\", \"account_settings\".\"interaction_policy_unlocked\", \"account_settings\".\"interaction_policy_public\" FROM \"account_settings\" WHERE (\"account_settings\".\"account_id\" = '01T3JH5K2DBYB5E2534EH910Y9')" requestID=000036g8n12dg0000080
timestamp="21/10/2025 21:25:00.811" func=bundb.queryHook.AfterQuery level=TRACE duration="364.035µs" query="SELECT \"account_stats\".\"account_id\", \"account_stats\".\"regenerated_at\", \"account_stats\".\"followers_count\", \"account_stats\".\"following_count\", \"account_stats\".\"follow_requests_count\", \"account_stats\".\"statuses_count\", \"account_stats\".\"statuses_pinned_count\", \"account_stats\".\"last_status_at\" FROM \"account_stats\" WHERE (\"account_stats\".\"account_id\" = '01T3JH5K2DBYB5E2534EH910Y9')" requestID=000036g8n12dg0000080
timestamp="21/10/2025 21:25:01.076" func=server.Start.Logger.func11.1 level=INFO latency=2.24364ms userAgent="Encyclia/0.0.1 (Fedify/1.9.0; Deno/2.5.4; +https://encyclia.pub)" method=GET statusCode=200 uri=/users/julian/main-key clientIP=2a01:4f8:c2c:9f00::1 requestID=000036g8n12dg000008g msg="OK: wrote 560B"
timestamp="21/10/2025 21:25:01.104" func=server.Start.Logger.func11.1 level=INFO latency=1.056401ms userAgent="Encyclia/0.0.1 (Fedify/1.9.0; Deno/2.5.4; +https://encyclia.pub)" method=GET statusCode=401 uri=/users/julian clientIP=2a01:4f8:c2c:9f00::1 errors="Error #01: AuthenticateFederatedRequest: http request wasn't signed or http signature was invalid\n" requestID=000036g8n12dg0000090 msg="Unauthorized: wrote 104B"
timestamp="21/10/2025 21:25:01.113" func=httpclient.(*Client).DoOnce level=ERROR method=GET url=https://encyclia.pub/0000-0001-7264-8496 attempt=1 requestID=000036g8n12dg0000080 pubKeyID=https://gts.jfietkau.me/users/julian/main-key msg="http response: 500 Internal Server Error"
timestamp="21/10/2025 21:25:01.113" func=search.(*Processor).byURI level=DEBUG requestID=000036g8n12dg0000080 msg="semi-expected error type looking up https://encyclia.pub/0000-0001-7264-8496 as account: enrichAccount: error dereferencing https://encyclia.pub/0000-0001-7264-8496: http response: 500 Internal Server Error (fast fail)"
timestamp="21/10/2025 21:25:01.115" func=bundb.queryHook.AfterQuery level=TRACE duration="558.802µs" query="SELECT \"status\".\"id\", \"status\".\"created_at\", \"status\".\"edited_at\", \"status\".\"fetched_at\", \"status\".\"pinned_at\", \"status\".\"uri\", \"status\".\"url\", \"status\".\"content\", \"status\".\"attachments\", \"status\".\"tags\", \"status\".\"mentions\", \"status\".\"emojis\", \"status\".\"local\", \"status\".\"account_id\", \"status\".\"account_uri\", \"status\".\"in_reply_to_id\", \"status\".\"in_reply_to_uri\", \"status\".\"in_reply_to_account_id\", \"status\".\"boost_of_id\", \"status\".\"boost_of_account_id\", \"status\".\"thread_id\", \"status\".\"edits\", \"status\".\"poll_id\", \"status\".\"content_warning\", \"status\".\"content_warning_text\", \"status\".\"visibility\", \"status\".\"sensitive\", \"status\".\"language\", \"status\".\"created_with_application_id\", \"status\".\"activity_streams_type\", \"status\".\"text\", \"status\".\"content_type\", \"status\".\"federated\", \"status\".\"interaction_policy\", \"status\".\"pending_approval\", \"status\".\"approved_by_uri\" FROM \"statuses\" AS \"status\" WHERE (\"status\".\"uri\" = 'https://encyclia.pub/0000-0001-7264-8496')" requestID=000036g8n12dg0000080
timestamp="21/10/2025 21:25:01.115" func=bundb.queryHook.AfterQuery level=TRACE duration="385.271µs" query="SELECT \"status\".\"id\", \"status\".\"created_at\", \"status\".\"edited_at\", \"status\".\"fetched_at\", \"status\".\"pinned_at\", \"status\".\"uri\", \"status\".\"url\", \"status\".\"content\", \"status\".\"attachments\", \"status\".\"tags\", \"status\".\"mentions\", \"status\".\"emojis\", \"status\".\"local\", \"status\".\"account_id\", \"status\".\"account_uri\", \"status\".\"in_reply_to_id\", \"status\".\"in_reply_to_uri\", \"status\".\"in_reply_to_account_id\", \"status\".\"boost_of_id\", \"status\".\"boost_of_account_id\", \"status\".\"thread_id\", \"status\".\"edits\", \"status\".\"poll_id\", \"status\".\"content_warning\", \"status\".\"content_warning_text\", \"status\".\"visibility\", \"status\".\"sensitive\", \"status\".\"language\", \"status\".\"created_with_application_id\", \"status\".\"activity_streams_type\", \"status\".\"text\", \"status\".\"content_type\", \"status\".\"federated\", \"status\".\"interaction_policy\", \"status\".\"pending_approval\", \"status\".\"approved_by_uri\" FROM \"statuses\" AS \"status\" WHERE (\"status\".\"url\" = 'https://encyclia.pub/0000-0001-7264-8496')" requestID=000036g8n12dg0000080
timestamp="21/10/2025 21:25:01.279" func=server.Start.Logger.func11.1 level=INFO latency="541.144µs" userAgent="Encyclia/0.0.1 (Fedify/1.9.0; Deno/2.5.4; +https://encyclia.pub)" method=GET statusCode=401 uri=/users/julian clientIP=2a01:4f8:c2c:9f00::1 errors="Error #01: AuthenticateFederatedRequest: http request wasn't signed or http signature was invalid\n" requestID=000036g8n12dg000009g msg="Unauthorized: wrote 104B"
timestamp="21/10/2025 21:25:01.285" func=httpclient.(*Client).DoOnce level=ERROR method=GET url=https://encyclia.pub/0000-0001-7264-8496 attempt=1 requestID=000036g8n12dg0000080 pubKeyID=https://gts.jfietkau.me/users/julian/main-key msg="http response: 500 Internal Server Error"
timestamp="21/10/2025 21:25:01.285" func=search.(*Processor).byURI level=DEBUG requestID=000036g8n12dg0000080 msg="semi-expected error type looking up https://encyclia.pub/0000-0001-7264-8496 as status: enrichStatus: error dereferencing https://encyclia.pub/0000-0001-7264-8496: http response: 500 Internal Server Error (fast fail)"
timestamp="21/10/2025 21:25:01.288" func=server.Start.Logger.func11.1 level=INFO latency=482.429847ms userAgent="Mozilla/5.0 (X11; Linux x86_64; rv:144.0) Gecko/20100101 Firefox/144.0" method=GET statusCode=200 uri=/api/v2/search?q=https:%2F%2Fencyclia.pub%2F0000-0001-7264-8496&resolve=true&limit=11 clientIP=2003:c5:8701:f175:79af:5e85:b498:ebf8 requestID=000036g8n12dg0000080 msg="OK: wrote 59B"
Steps to Reproduce
- Set up a Fedify server with an actor dispatcher.
- Add a trivial
AuthorizePredicate:
federation
.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => {
// Omitted for brevity
}).authorize((_ctx, _identifier) => true); // Allow all requests
- Attempt to access the actor from a remote server using a signed request.
- Observe that the key-owning actor from the remote server is always fetched using an unsigned request.
- If the remote server does not permit unsigned actor fetches, observe an uncaught
FetchErrorthat results in the server framework (Deno in my case) serving an HTTP 500 error in response to the original remote request.