Skip to content

Custom collections with symbol names can be neither served nor parsed #1144

Description

@dahlia

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Fields

Priority

None yet

Effort

None yet

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions