Hydration Proof

Search documentation

Find a page or section

The problems people hit most, and what to do about each.

hydration-proof troubleshooting starts with npx hydration-proof doctor, which checks Node.js, Playwright, the browsers, the config file and framework detection in one go. The answers below cover the problems people hit most, grouped by where they show up: installing and starting, signing in, routes, findings, source locations and speed.

Start hydration-proof troubleshooting with doctor

npx hydration-proof doctor

It prints one line per check, marked as passed, a warning or a failure:

CheckWhat it tells you
Node.jsThe running version; 22.18 or newer is required
Package manager, CIWhat was detected; CI changes defaults such as retries
PlaywrightThe playwright-core version in use and where it came from
chromium, firefox, webkitWhether each browser is installed, with the command that installs it
ConfigThe config file it found, or the command that creates one
FrameworkThe adapter that was detected, or "not detected (set server.command)"
RoutesHow many static and dynamic routes the adapter discovered

doctor exits with 0 when nothing failed and 2 when something did. A missing Firefox or WebKit is only a warning; a missing Chromium is a failure.

Installing and starting the app

Why does it say "chromium is not installed"?

The browser for the Playwright version hydration-proof uses has not been downloaded yet, and the run stops with exit code 4. Install it:

npx hydration-proof install
npx hydration-proof install --with-deps  # on Linux CI, with system libraries
npx hydration-proof install firefox webkit

Why does it say "Nothing answers at …"?

The app could not be built, started or reached, and the run exits with code 3. Check these in order:

  1. Run server.command on its own. {port} in the command is replaced with the port hydration-proof chose, and the same port is in the PORT environment variable. A server that ignores both listens somewhere else.
  2. Give slow builds more time. server.timeout is how long to wait for the app to answer, 120000 ms by default.
  3. With --url, start the app first. Nothing is built or started when you pass a URL.

When a build or start command fails, the run prints the command and the end of its output.

What if hydration-proof does not know how to start the app?

No adapter matched, so there is no start command. Set server.command (and server.build) in hydration-proof.config.ts, or pass --url to test an app you started yourself. Adapters shows a config for a custom server.

Why does Next.js development mode not load?

hydration-proof opens the development server on localhost, because Next.js blocks development resources for other hosts. If you test a development server through another host name, add that host to allowedDevOrigins in next.config.

Why does my TypeScript config fail to load?

Node.js loads hydration-proof.config.ts directly, without a compiler. Use plain types (no enum or namespace), include the file extension in relative imports (./routes.ts), or rename the file to .mjs.

Signing in

Why do signed-in pages end on the login page (HP9010)?

The scenario's login did not leave a working session, so the page redirected and got an HP9010 warning.

  1. Run with --headed --workers 1 to watch the login.
  2. Check that login waits for the page after signing in, for example with page.waitForURL.
  3. Check that the session cookie is not limited to another domain.

If the route is supposed to redirect, set expectRedirect on it. See scenarios and sign-in.

What does "The login of scenario … failed" or "The setup hook failed" mean?

The scenario's login function, or the hooks.setup function, threw an error; its message follows. Both run after the app is up, with the same baseUrl the pages use. Either failure stops the run with exit code 2.

Routes

Why are some dynamic routes skipped?

A dynamic route is only tested with example values: from routes.dynamic, or from pages the build pre-rendered. A route without values is skipped, with a note that lists it. Add values, or list the URLs in a sitemap and turn on routes.sitemap:

hydration-proof.config.ts
import { defineConfig } from "hydration-proof";
 
export default defineConfig({
  routes: { dynamic: { "/products/[id]": ["1", "42"] } },
});

Why is a new page missing from the run?

Discovered routes are cached until the build or a route folder changes. Run with --no-cache if a change was missed:

npx hydration-proof test --no-cache

Findings

How do I keep a difference that is intentional?

Mark the element with data-hydration-proof-ignore, add a selector to ignore.selectors, or add an ignore.issues rule with a reason. For values that differ on purpose, such as a live clock, render them after mount or put suppressHydrationWarning on that element; hydration-proof then lists them as info. Ignoring findings covers each option.

What if a page keeps changing and never settles (HP9009)?

Pages with animations, polling or live data may never be quiet for the 400 ms hydration-proof waits by default, and get an HP9009 warning. Tell it what "ready" means for the page instead:

hydration-proof.config.ts
import { defineConfig } from "hydration-proof";
 
export default defineConfig({
  ready: {
    selector: "#app-ready", // an element that exists once the page is ready
    quietMs: 200, // or wait less time without changes
  },
});

ready.function takes a page function (as a string) that must return a truthy value. A route object can have its own ready options.

Why is a cause "not proven"?

Probes prove a cause when a finding changes with exactly one factor. When the page renders differently on identical reloads, the value comes from the server or an API, for example a counter or the current time on the server, which the browser cannot control. Send the data the server rendered with to the client.

Why do navigation checks report differences?

Client navigation keeps state that a direct load does not have: module variables, context in layouts, parallel route slots. If the difference is intended (a modal from an intercepting route, a slot that keeps its page), add an ignore rule for the route with code: "HP5004". To start from another page, set checks.navigation.from, or navigateFrom on the route.

What is a lost click?

A server-rendered button is visible before React can handle its clicks, and a click in that window does nothing (HP5001). Show the control as disabled or loading until the page is interactive, or make it work without JavaScript. If that is acceptable for your app, ignore HP5001.

Source locations

Why is there no source location in production?

The issue's sourceUnavailableReason says why. Enable browser source maps (productionBrowserSourceMaps: true in Next.js) to get the component, or run with --mode development for the exact line. Elements rendered by Server Components have no client code to point at.

Why are source maps from my CDN not used?

Scripts and source maps are only fetched from the app's own origin, and the run ends with a note that names the origin it refused. Add the CDN to sourceOrigins. Security explains why the restriction exists.

Why are component names single letters?

Production builds minify component names. Run with --mode development to see the real names:

npx hydration-proof test --mode development

Speed

How do I make the matrix faster?

Every environment loads every route. Keep the default strategy: "pairwise", lower matrix.max, limit the matrix to some scenarios with matrix.scenarios, or run quick local checks with --no-matrix. In CI, split the pages across jobs with --shard. See the environment matrix.

Why is network throttling different in Firefox and WebKit?

Only Chromium can throttle the network. In Firefox and WebKit, every request except the page itself is delayed by the latency instead, which turns the HTTP cache off. CPU slowdown is Chromium only.