Skip to content

Start: provide a supported way to unit-test server functions through the pipeline (inputValidator is skipped on direct calls) #7507

@andreialecu

Description

@andreialecu

Which project does this relate to?

Start

Describe the problem

There's no supported way to unit-test that a server function's .inputValidator() runs. Calling the fn directly in a Node test (await fn({ data })) silently skips the validator and invokes the handler with raw, unvalidated data.

For apps that rely on .inputValidator(zodSchema) as their wire-level input guard (e.g. NoSQL‑injection hardening, where the schema coerces ids/queries to primitives before they reach the database), this means the exact security boundary we most want to regression-test is the one a unit test can't reach. A test that passes a Mongo operator object as an id "succeeds" (the handler runs against it) instead of being rejected — the opposite of what production does.

This came up while migrating a codebase to the idiomatic .inputValidator(Schema) form and wanting tests that guard against someone reverting a validator back to a type-only (data) => data passthrough.

Root cause

In createServerFn, calling the returned fn runs the middleware chain with env === "client":

// createServerFn.js
return Object.assign(async (opts) => {
  const result = await executeMiddleware(resolvedMiddleware, "client", { ... });
  ...
});

but execValidator only runs on the server branch:

if ("inputValidator" in nextMiddleware.options && nextMiddleware.options.inputValidator && env === "server")
  ctx.data = await execValidator(nextMiddleware.options.inputValidator, ctx.data);

So in a Node test there's no client/server transport — the handler runs, but the validator (the "server"-only step) is skipped. Confirmed empirically: a server fn called directly with an operator-object id reaches Model.findOne() with the raw object, whereas through the server pipeline the validator rejects it first.

Current workaround (and why it's not enough)

The only way to exercise the validator today is to drive the "server" path via the fn's internal __executeServer, inside a hand-built start context:

import { runWithStartContext } from '@tanstack/start-storage-context';

function callServerFn(fn, data) {
  return runWithStartContext(
    {
      contextAfterGlobalMiddlewares: {},
      request: new Request('http://localhost'),
      executedRequestMiddlewares: new Set(),
    },
    () => fn.__executeServer({ data, context: {} }), // __-prefixed internal
  );
}
// → resolves to { result, error }; a validator rejection surfaces as `error`
//   and short-circuits the handler.

Problems with relying on this:

  • __executeServer is an undocumented __-prefixed internal that can change between releases.
  • The start-context shape is reverse-engineered from the source.
  • Global function-middleware isn't applied (also noted by others in the discussion below).

Proposed solution

A supported, exported testing entry — e.g. createServerFnTestCaller() / callServerFn(fn, { data, context }) — that runs the full server pipeline (global middleware + inputValidator + handler) and returns { result, error }, so apps can assert both happy-path and validation-rejection behavior without reaching into internals.

References

Platform

  • Router / Start Version: @tanstack/react-start 1.167.x

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions