Summary
Custom collection dispatchers accept a symbol as their name, but a collection registered with a symbol name can be neither served nor parsed. Federation.fetch() responds to its URI with 404 Not Found, and Context.parseUri() throws a TypeError for it. With createFederation(), only Context.getCollectionUri() works, so an application can build the URI of such a collection but nothing else can use it. With a FederationBuilder, even Context.getCollectionUri() throws a RouterError once the builder is built.
Steps to reproduce
import { createFederation, MemoryKvStore } from "@fedify/fedify";
import { Object } from "@fedify/vocab";
const bookmarks = Symbol("bookmarks");
const federation = createFederation<void>({ kv: new MemoryKvStore() });
federation.setCollectionDispatcher(
bookmarks,
Object,
"/users/{identifier}/bookmarks",
() => ({ items: [] }),
);
const ctx = federation.createContext(new URL("https://example.com/"));
const uri = ctx.getCollectionUri(bookmarks, { identifier: "alice" });
console.log(uri.href); // https://example.com/users/alice/bookmarks
const response = await federation.fetch(
new Request(uri, { headers: { Accept: "application/activity+json" } }),
{ contextData: undefined },
);
console.log(response.status); // 404
ctx.parseUri(uri);
// TypeError: Cannot read properties of undefined (reading 'typeId')
With a FederationBuilder, building the URI fails as well:
import { createFederationBuilder, MemoryKvStore } from "@fedify/fedify";
import { Object } from "@fedify/vocab";
const bookmarks = Symbol("bookmarks");
const builder = createFederationBuilder<void>();
builder.setCollectionDispatcher(
bookmarks,
Object,
"/users/{identifier}/bookmarks",
() => ({ items: [] }),
);
const federation = await builder.build({ kv: new MemoryKvStore() });
const ctx = federation.createContext(new URL("https://example.com/"));
ctx.getCollectionUri(bookmarks, { identifier: "alice" });
// RouterError: No collection dispatcher registered for "Symbol(bookmarks)".
Expected behavior
Federation.fetch() dispatches the request to the collection dispatcher and responds with the collection, as it does for a collection with a string name.
Context.parseUri() returns { type: "collection", name: bookmarks, ... } with the original symbol as name.
Context.getCollectionUri() works on a federation built from a FederationBuilder as well.
Actual behavior
Federation.fetch() responds with 404 Not Found.
Context.parseUri() throws TypeError: Cannot read properties of undefined (reading 'typeId').
- On a federation built from a
FederationBuilder, Context.getCollectionUri() throws RouterError: No collection dispatcher registered for "Symbol(bookmarks)".
Cause
FederationBuilderImpl registers the route of a symbol-named collection under a generated UUID, e.g., collection:1b4e28ba-…, because a symbol cannot be part of a route name. It stores the callbacks and the item type under the symbol itself, though:
collectionCallbacks[name] and collectionTypeIds[name] in packages/fedify/src/federation/builder.ts are keyed by the symbol.
FederationImpl.fetch() and ContextImpl.parseUri() in packages/fedify/src/federation/middleware.ts take the name back out of the route name, which is the UUID string, and look it up in these maps.
The lookup therefore yields undefined. fetch() passes the missing callbacks on and ends up with 404 Not Found, while parseUri() reads typeId from the missing class and throws. The existing tests only check that symbol names are registered as distinct dispatchers, not that they are dispatched or parsed.
The symbol-to-UUID map itself, #symbolRegistry, is a private field of FederationBuilderImpl that build() does not copy to the new Federation. The built federation therefore starts with an empty registry, and getCollectionPath() generates a new UUID for the symbol, which matches none of the routes. This is why Context.getCollectionUri() fails only with a builder.
A fix probably needs both a reverse map from the UUID to the original name, used wherever a route name is turned back into a collection name, and copying the registry in build().
Affected versions
Every version since 1.8.0, which added symbol names for custom collection dispatchers. The lookups by the UUID string are on every supported maintenance branch, from 2.0-maintenance to 2.3-maintenance, and on main, so the fix should target 2.0-maintenance. Both reproductions above were run on main with Deno; the FederationBuilder case has not been checked on the maintenance branches. The code path does not depend on the runtime.
Tests
- Serving a symbol-named collection and an ordered collection through
Federation.fetch(), including their pages.
Context.parseUri() returning the original symbol as name for both.
- Two different symbols with the same description, e.g., two
Symbol("bookmarks") values, staying distinct.
- The same through a
FederationBuilder built into a Federation.
Summary
Custom collection dispatchers accept a symbol as their name, but a collection registered with a symbol name can be neither served nor parsed.
Federation.fetch()responds to its URI with404 Not Found, andContext.parseUri()throws aTypeErrorfor it. WithcreateFederation(), onlyContext.getCollectionUri()works, so an application can build the URI of such a collection but nothing else can use it. With aFederationBuilder, evenContext.getCollectionUri()throws aRouterErroronce the builder is built.Steps to reproduce
With a
FederationBuilder, building the URI fails as well:Expected behavior
Federation.fetch()dispatches the request to the collection dispatcher and responds with the collection, as it does for a collection with a string name.Context.parseUri()returns{ type: "collection", name: bookmarks, ... }with the original symbol asname.Context.getCollectionUri()works on a federation built from aFederationBuilderas well.Actual behavior
Federation.fetch()responds with404 Not Found.Context.parseUri()throwsTypeError: Cannot read properties of undefined (reading 'typeId').FederationBuilder,Context.getCollectionUri()throwsRouterError: No collection dispatcher registered for "Symbol(bookmarks)".Cause
FederationBuilderImplregisters the route of a symbol-named collection under a generated UUID, e.g.,collection:1b4e28ba-…, because a symbol cannot be part of a route name. It stores the callbacks and the item type under the symbol itself, though:collectionCallbacks[name]andcollectionTypeIds[name]in packages/fedify/src/federation/builder.ts are keyed by the symbol.FederationImpl.fetch()andContextImpl.parseUri()in packages/fedify/src/federation/middleware.ts take the name back out of the route name, which is the UUID string, and look it up in these maps.The lookup therefore yields
undefined.fetch()passes the missing callbacks on and ends up with404 Not Found, whileparseUri()readstypeIdfrom the missing class and throws. The existing tests only check that symbol names are registered as distinct dispatchers, not that they are dispatched or parsed.The symbol-to-UUID map itself,
#symbolRegistry, is a private field ofFederationBuilderImplthatbuild()does not copy to the newFederation. The built federation therefore starts with an empty registry, andgetCollectionPath()generates a new UUID for the symbol, which matches none of the routes. This is whyContext.getCollectionUri()fails only with a builder.A fix probably needs both a reverse map from the UUID to the original name, used wherever a route name is turned back into a collection name, and copying the registry in
build().Affected versions
Every version since 1.8.0, which added symbol names for custom collection dispatchers. The lookups by the UUID string are on every supported maintenance branch, from 2.0-maintenance to 2.3-maintenance, and on main, so the fix should target 2.0-maintenance. Both reproductions above were run on main with Deno; the
FederationBuildercase has not been checked on the maintenance branches. The code path does not depend on the runtime.Tests
Federation.fetch(), including their pages.Context.parseUri()returning the original symbol asnamefor both.Symbol("bookmarks")values, staying distinct.FederationBuilderbuilt into aFederation.