---
title: Troubleshooting
description: Diagnose failed c15t consent prefetch and build-time manifest
  downloads in Next.js, verify manifest requests, and fix a missing
  c15t.config.ts, config errors, unknown server location and prerendering
  errors.
group: frameworks
lastModified: "2026-10-10T16:01:45+01:00"
---
## Why does the page render when consent prefetch fails?

**Check.** `resolveConsent` returns its baseline state when a manifest or
backend request fails. The page can still render, and the client retries
initialization. Until policy resolution succeeds, optional categories stay
denied and the stock banner stays hidden. Check the failed URL and response in
your server logs, as browser Network tools cannot show a server-side fetch.

**Fix.** Pass `onError` to `resolveConsent` to report failures in production.
Without it, the helper logs a warning only outside production.

**Check.** If the consent route repeatedly calls itself, check its upstream
URL.

**Fix.** Set the config's `backendURL`, or `NEXT_PUBLIC_C15T_BACKEND_URL` (or
`NEXT_PUBLIC_INTH_PROJECT_URL`), to
the absolute Inth or self-hosted backend endpoint. To send the browser through
`/api/c15t`, set `proxy: true` and `routePrefix` instead of a relative
`backendURL`; the route keeps the absolute URL. See
[Next.js optimization](/docs/frameworks/next/optimization#keep-browser-consent-requests-on-your-origin).

## Why does `next build` or `next dev` fail to fetch the manifest?

**Check.** `withConsentManifest` downloads `${backendURL}/manifest` when
`next build`, `next dev` or `next typegen` loads `next.config.ts`. When the
download fails or takes longer than 10 seconds, `next build` stops with an
error that starts with `@c15t/nextjs/build: could not fetch the consent manifest from <url> during the build`. `next dev` logs the same message as a
warning, and `snapshot` from `c15t/generated` is `undefined`, so the server
fetches the policy at runtime. It never reuses an old snapshot. The part in
parentheses names the cause:

* `fetch failed`, with `ENOTFOUND`, `ECONNREFUSED` or another network error,
  or `no response within 10 seconds`. The build machine cannot reach the
  backend.
* `/manifest responded 404`, or another status. The URL is not your
  project's backend. Check for the `https://your-project.inth.app`
  placeholder and a missing path prefix.
* `/manifest returned an invalid consent manifest.` The URL answered, but not
  with a c15t manifest. It may be a different service, or a backend that does
  not support this c15t version.

`no backend URL is set` means the build found no `backendURL` option, no
`backendURL` in `c15t.config.ts` and no `NEXT_PUBLIC_C15T_BACKEND_URL` or
`NEXT_PUBLIC_INTH_PROJECT_URL`, or empty ones. `next build` stops on it and
`next dev` warns. A notice that the build `skipped the consent manifest fetch`
means the backend URL is a path such as `/api/c15t`. With
`onBuildError: 'fail'`, that stops the build with `build-time manifests require an absolute upstream URL.` The same notice ending in
`because c15t.config.ts uses hosted()`, `uses offline()`,
`passes manifest({ snapshot })` or `sets manifest({ source: 'runtime' })` is
expected: those modes read no build-time manifest.

`@c15t/nextjs/build: could not read c15t.config.ts (...)` is a warning: the
build could not evaluate the file, so it fetched the manifest as for
`manifest()`, whatever the file's `mode`. The part in parentheses is the
error. The usual cause is an import Node can't load at build time, such as a
stylesheet. Keep `c15t.config.ts` to the config and the packages it needs.

**Fix.** Set `NEXT_PUBLIC_C15T_BACKEND_URL` (or
`NEXT_PUBLIC_INTH_PROJECT_URL`) to the absolute backend URL from your Inth
project, or your self-hosted origin with its path prefix, wherever
`next build` runs, including CI. To deploy while the backend is down, run the
build with `C15T_ON_BUILD_ERROR=runtime`, or set `onBuildError: 'runtime'`.
The deployment then works without a snapshot, and each server fetches the
policy on its first request. To drop the build-time fetch, use
[runtime manifest caching](/docs/frameworks/next/optimization#reuse-cached-policy-data)
instead.

## Why does importing `c15t/generated` from a client component fail the build?

**Check.** `next build` or `next dev` reports that `server-only` cannot be
imported from a Client Component, in a module that imports `c15t/generated`.

**Fix.** `withConsentManifest` keeps the snapshot out of browser bundles, so
`c15t/generated` only works in server code: Server Components, route
handlers, `getServerSideProps` and API routes. Most apps never import it.
`resolveConsent` and the consent route read the snapshot themselves, and
`ConsentRoot` receives the resolved state.

## Why is `snapshot` undefined?

**Check.** `snapshot` from `c15t/generated` is `undefined` in server code.

**Fix.** Check these causes:

* `withConsentManifest` doesn't wrap the config in `next.config.ts`. Without
  it, `c15t/generated` still resolves, but exports `undefined`.
* The fetch failed in `next dev`, or in a build with
  `C15T_ON_BUILD_ERROR=runtime`. The server then fetches the policy at
  runtime.
* The backend URL is relative, or the config sets `output: 'export'`, so the
  build skipped the fetch.
* The code runs in a package listed in `serverExternalPackages`, which stays
  outside the bundle and doesn't see the alias.

`c15t/generated` ships its own types, so `tsc --noEmit` passes on a fresh
clone without running Next.js first.

## Verify manifest fetching

Check the Next.js server and browser requests after following the
[App Router](/docs/frameworks/next/app-router) or
[Pages Router](/docs/frameworks/next/pages-router) guide:

1. Load the page twice. A warm manifest setup resolves policy without calling
   the upstream backend `/init`. A browser request to your local
   `/api/c15t/init` is expected when client initialization is needed.
2. Make a consent choice. Confirm the submission reaches the backend
   `/subjects` endpoint and reload to check that the choice persists.
3. Test two locations with different configured policies. Confirm the resolved
   policy matches each location and that your host passes trusted geography
   headers to Next.js.
4. Change a policy, rebuild, then check it. With runtime fetching, check it
   after the manifest caches refresh instead. Cache the public manifest, never
   the visitor-specific init response.
5. If you set `proxy: true`, confirm browser consent requests use `/api/c15t`,
   including submissions. Direct requests to the backend are expected without
   it.

## Why does ConsentRoot warn that it found no config?

**Check.** The browser console shows this warning outside production:

```txt
[c15t] ConsentRoot found no config. Export `defineConsentConfig()` as the default export of c15t.config.ts at the project root and wrap next.config.ts in `withConsentManifest()`, or pass `config`.
```

**Fix.** `withConsentManifest` finds `c15t.config.ts`, `.mts`, `.js` or
`.mjs` in the directory `next build` and `next dev` run from, and nowhere
else. A config in `src/` is not found. Check that:

* `next.config.ts` exports `withConsentManifest(nextConfig)`.
* The file is at the project root, next to `next.config.ts`.
* It has a default export, `export default defineConsentConfig({ ... })`, not
  only a named one.
* You restarted `next dev` after adding the file.

Without a config, `ConsentRoot` reads `NEXT_PUBLIC_C15T_BACKEND_URL`, which
`withConsentManifest` sets from `NEXT_PUBLIC_INTH_PROJECT_URL` when only that
is set, uses
`manifest()` and runs no `scripts`. With no backend URL either, it throws; see
the next section.

## Why does ConsentRoot throw that it needs a backend URL?

**Check.** The page, or `next build` while it prerenders the page, throws one
of these errors from `ConsentRoot`:

* `` @c15t/nextjs: manifest() needs a backend URL. Set NEXT_PUBLIC_C15T_BACKEND_URL (or NEXT_PUBLIC_INTH_PROJECT_URL), or `backendURL` in c15t.config.ts. ``
* `` @c15t/nextjs: hosted() needs a backend URL. Set NEXT_PUBLIC_C15T_BACKEND_URL (or NEXT_PUBLIC_INTH_PROJECT_URL), or `backendURL` in c15t.config.ts. ``

A production build prints only the first sentence, such as
`@c15t/nextjs: manifest() needs a backend URL.`

`ConsentRoot` found no `c15t.config.ts`, or got a `config` prop without a
backend URL, and neither `NEXT_PUBLIC_C15T_BACKEND_URL` nor
`NEXT_PUBLIC_INTH_PROJECT_URL` was set when Next.js compiled the page.

**Fix.** Set `NEXT_PUBLIC_C15T_BACKEND_URL` (or
`NEXT_PUBLIC_INTH_PROJECT_URL`) wherever `next build` and
`next dev` run, and check that `withConsentManifest` finds `c15t.config.ts`
(see the previous section). Earlier alphas fell back to offline mode here;
now the root throws. To run without a backend, set `mode: offline()` in
`c15t.config.ts`, or pass `options={{ mode: offline() }}` to `ConsentRoot`.

## Why does ConsentRoot warn that a transport ships in the first-load bundle?

**Check.** The browser console shows a warning such as:

```txt
[c15t] ConsentRoot `mode` is the hosted() transport itself, so its code ships in the first-load bundle. Use hosted() from c15t/next instead: it is plain data, and the root loads the code only when it runs.
```

**Fix.** `options.mode` received `hosted()`, `offline()` or `manifest()` from
`c15t/react`, which are transports. Import the mode from `c15t/next` and set
it as `mode` in `c15t.config.ts`. `custom(transport)` does not warn.

## Why does defineConsentConfig throw?

**Check.** `next build`, `next dev` or the first server render fails with a
`TypeError` that starts with `@c15t/nextjs: defineConsentConfig`.
`withConsentManifest` reads `c15t.config.ts` when Next.js loads
`next.config.ts`, so most config errors stop the build before it compiles.
The checks run on the server only; the browser copy of the file skips them.

**Fix.** The [config reference](/docs/frameworks/next/data-fetching-reference#config-reference)
lists each message. The most common is
`` @c15t/nextjs: defineConsentConfig needs `backendURL`, or NEXT_PUBLIC_C15T_BACKEND_URL (or NEXT_PUBLIC_INTH_PROJECT_URL) set at build time. ``
Set the variable in `.env.local` and in your host's build settings, or pass
`backendURL`.

## Why is the visitor's location unknown on the server?

**Check.** `resolveConsent` and the consent route read the visitor's
country and region from hosting platform headers such as `cf-ipcountry` and
`x-vercel-ip-country`; c15t never derives location from an IP address itself.
Some platforms expose those headers to Next.js middleware or proxy but strip
them before Server Components and Route Handlers run, so every visitor falls
back to your unknown-location rule. Log
`(await headers()).get('x-vercel-ip-country')`, or your host's equivalent, in
a Server Component.

**Fix.** If the header is missing there, add `c15tProxy` as described in
[Forward geography headers](/docs/frameworks/next/geography-headers). It
forwards the resolved values as `x-c15t-country` and `x-c15t-region`, which
take precedence everywhere c15t reads location.

## Why does Next.js report an unstable `Date.now()` during prerendering?

**Check.** The error
`Next.js encountered the unstable value Date.now() while prerendering`,
pointing at the `await resolveConsent(...)` line, comes from
`partialPrefetching: true` combined with `cacheComponents: true` (Next.js
16.3 and later). During the runtime-prefetch stage Next.js resolves
`headers()` and then flags the `Date.now()` that follows. It only appears in
`next dev`; `next build` passes because build-time prerenders never resolve
`headers()`.

**Fix.** Use the current c15t release. `resolveConsent` handles this itself:
the default App Router request reader calls `await connection()` from
`next/server` before reading the clock, so the component is already
request-time when the clock is read. You do not need to add `connection()` to
your layout. If you still see the error on the current release, report your
Next.js version and config on
[c15t/c15t#1107](https://github.com/c15t/c15t/issues/1107).

## Why does `next build` fail on a route with `ensureStatic = 'navigation'`?

**Check.** The build fails with one of these errors, and the route sits under a
root layout that calls `resolveConsent`:

* `Next.js encountered data that is not available during a static prerender, but is unable to provide a location` (streamed layout)
* `Next.js encountered uncached or runtime data on a route that must be fully static` (awaited layout)

`'navigation'` requires the whole route to be static, including the root
layout. `resolveConsent` reads the visitor's cookies and headers, so that
layout can't be.

**Fix.** Move those pages into their own route group with the browser layout,
which renders `ConsentRoot` without `state`. See
[Keep pages static with `ensureStatic`](/docs/frameworks/next/rendering#keep-pages-static-with-ensurestatic).
`'shell'` and `'prefetch'` work with the server layouts.

## Why does the page throw that no IABProvider is mounted?

The visitor's policy uses the `iab` model and the backend sent its vendor
list, but no `IABProvider` is mounted. The standard banner and dialog do
not handle the IAB model, so they throw rather than leave the visitor
with no consent UI. Server and browser render the same error.

Fix it one of two ways:

* If the site uses IAB TCF, render `IABProvider` with `IABConsentBanner`
  and `IABConsentDialog` from `c15t/react/iab` inside your consent
  provider. They can sit next to the standard banner.
* If it does not, remove the `iab` model from the policy for that region
  in your Inth project or policy pack.

A backend that answers `gvl: null` turns IAB off for that request. The
policy then runs as opt-in and nothing throws.

The check passes once `c15t/react/iab` has loaded. If you import the IAB
components with a dynamic `import()`, a render of the standard banner
that happens before the import finishes still throws.

## Inspect the active policy

Place this diagnostic component inside your existing consent
boundary. It reads state without recording a choice.

```tsx title="app/consent-policy-debug.tsx"
'use client';

import { useSnapshot } from 'c15t/next';

export function ConsentPolicyDebug() {
	const snapshot = useSnapshot();
	const resolution = snapshot.resolution;

	return (
		<pre>
			{JSON.stringify(
				{
					status: resolution.status,
					policy: resolution.status === 'matched' ? resolution.policy.id : null,
					prompt: snapshot.promptRequirement,
					permissions: snapshot.effectivePermissions,
					location: snapshot.location,
					privacySignals: snapshot.privacySignals,
				},
				null,
				2
			)}
		</pre>
	);
}
```

The diagnostic subscribes to consent changes. Remove it after verification.
For a production feature gate, use `useConsent('marketing')` or the category
your feature needs instead of subscribing to the whole snapshot.

## More help

[Troubleshoot consent](/docs/guides/troubleshooting) covers problems shared by
every framework, such as a missing banner, analytics that load before a choice,
imports that fail because npm installed c15t v2, choices that disappear on
reload, server HTML that differs from the browser, static builds that fail and
content blockers that hide the consent UI.
