clockworklabs/SpacetimeDB · error · TypeError

HTTP route for path ' ' refers to a handler that was not…

Error message

HTTP route for path '${route.path}' refers to a handler that was not exported.

What it means

When `resolveHttpRoutes` runs, every route registered on a Router must point at a handler object that was registered as an HTTP handler export — the lookup is `httpHandlerExports.get(route.handler)`, which is only populated when the handler's `[registerExport]` runs during module export registration. If you passed the raw function, a handler built by a different (or no) schema registration, or never exported the handler, the route cannot be resolved and the build fails with 'refers to a handler that was not exported'.

Solutions

  1. Pass the exact object returned by `spacetimedb.httpHandler(...)` to the router methods, and export that same object from the module: `export const listUsers = spacetimedb.httpHandler(...); router.get('/users', listUsers)`.
  2. Do not wrap the handler in another function or pass `handler[httpHandlerFn]`-style internals.
  3. Check that the export name in the module matches a real `export` statement; registration only happens for exported names (the `default` export name is skipped).

Example fix

// before
function listUsers(ctx) { ... }
export default new Router().get('/users', listUsers); // raw fn, not an export

// after
export const listUsers = spacetimedb.httpHandler(schema, undefined, ctx => { ... });
export default new Router().get('/users', listUsers);
Defensive patterns

Strategy: validation

Validate before calling

// Verify every router route points at a real handler export before publishing.
const handlerExports = new Set<unknown>([listUsers, getUser]);
function assertRouterHandlers(router: Router) {
  for (const route of router.intoRoutes()) {
    if (!handlerExports.has(route.handler)) {
      throw new Error(`route ${route.path} uses an unexported handler`);
    }
  }
}

Type guard

function isHttpHandlerExport(x: unknown, schemaExports: Set<unknown>): x is Function {
  return typeof x === 'function' && schemaExports.has(x);
}

Prevention

When it happens

Trigger: Passing the inner function instead of the export object to `router.post(path, fn)`; creating a handler with `spacetimedb.httpHandler(...)` but not including it in the module's `export`s; passing a handler created lazily/inside a function so registration never happened when routes resolved.

Common situations: Refactoring a handler into a helper that returns the fn, then wiring the fn into the router; forgetting `export const` on the handler; handlers created in a loop where the export object is discarded.

Related errors


AI-assisted analysis of clockworklabs/SpacetimeDB@eddf9f5014 (2026-08-20). Data as JSON: /api/errors/04de59dd5e0defaa. Report an issue: GitHub.

Appendix: source

Thrown at crates/bindings-typescript/src/server/schema.ts:212

        throw new TypeError(
          `Table ${tableName} defines multiple schedules: ${existingFunctionName} and ${functionName}. A schedule table can only be used by one reducer or procedure.`
        );
      }
      scheduledTables.set(tableName, functionName);
      this.moduleDef.schedules.push({
        sourceName: undefined,
        tableName,
        scheduleAtCol,
        functionName,
      });
    }
  }

  resolveHttpRoutes() {
    for (const route of this.pendingHttpRoutes) {
      const handlerFunction = this.httpHandlerExports.get(route.handler);
      if (handlerFunction === undefined) {
        throw new TypeError(
          `HTTP route for path '${route.path}' refers to a handler that was not exported.`
        );
      }
      this.moduleDef.httpRoutes.push({
        handlerFunction,
        method: route.method,
        path: route.path,
      });
    }
  }
}

type PendingSchedule = {
  table: UntypedTableSchema;
  tableName?: string;
  scheduleAtCol?: number;
  reducer?: () => UntypedScheduledFunctionExport;
  functionName?: string;

View on GitHub (pinned to eddf9f5014)