Skip to content

New route FOLDER silently dropped via Fastify's sealed plugin tree, with no restart signal #2732

Description

@kriszyp

A route folder that is new to a running process is silently dropped, permanently, until a restart. This is the second of the two mechanisms behind the "deployed route 404s" class; #1934 fixed the first one and did not touch this one.

Mechanism

server/fastifyRoutes.ts registers each route folder exactly once, guarded by a module-scope routeFolders Set:

if (!routeFolders.has(routeFolder)) {
    routeFolders.add(routeFolder);
    try {
        resolvedServer.register(buildRouteFolder(routeFolder, prefix));
    } catch (error) {
        if (error.message === 'Root plugin has already booted')
            harperLogger.warn(
                `Could not load root fastify route for ${entry.absolutePath}, this may require a restart to install properly`
            );
        else throw error;
    }
}

Once Fastify's root plugin tree has booted, register() throws Root plugin has already booted. The catch swallows it with a warn — and note the Set has already been mutated on the line above, so the folder is now recorded as registered even though registration failed. There is no retry, no queue, no self-heal: the route 404s for the life of the process, and a later deploy of the same folder takes the routeFolders.has(...) early-out.

Why #1934's fix does not cover this

#1934 was closed by #1936 ("preserve entry events across deploy scans"). That diff touches EntryHandler.ts, RuntimeModuleTracker.ts, Scope.ts, componentLoader.ts, deployLifecycle.ts, jsResource.ts, roles.ts, jsLoader.ts, static.ts — not server/fastifyRoutes.ts. What it fixed is entry-event classification: an EntrySnapshot digest diff so a redeployed known file surfaces as change (triggering scope.requestRestart()) instead of a spurious add that the old if (entry.eventType !== 'add') gate silently swallowed. That is the "new FILE in an already-known folder" case.

A genuinely new folder is correctly classified add even after #1936, and then hits Fastify's own register-after-boot constraint, which is a different layer entirely. Verified on origin/main (0db6cabe6, 2026-09-03) — the code above is current.

Prior art

#674 (closed) covered the user-visible symptom of the same situation — a TypeError: rg.getMatch is not a function when hitting a not-yet-registered component URL — and its accepted fix was to return an informative "restart required" response. That treats restart-required as expected. This issue is about the layer beneath: the registration attempt fails and is swallowed, so nothing ever retries it.

Impact

Deploying a component that adds a new routes folder appears to succeed (deploy_component returns success), and the routes 404. The only signal is a single warn line. It does not self-heal on subsequent deploys of the same component, because the Set entry persists; only a process restart clears it.

Suggested direction

Do not add to routeFolders until register() has actually succeeded, and either queue the folder for registration at the next safe point or make the deploy path surface a hard "restart required" result rather than a warn. Decide deliberately whether the contract is "new route folders require a restart" (in which case say so loudly at deploy time) or "new route folders register live" (in which case the register-after-boot constraint needs handling).

Found by the qa-explorer campaign (F-230).

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Fields

    Priority

    P1

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions