Next.js Verify and troubleshoot
Troubleshooting
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.
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, withENOTFOUND,ECONNREFUSEDor another network error, orno 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 thehttps://your-project.inth.appplaceholder 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
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:
withConsentManifestdoesn't wrap the config innext.config.ts. Without it,c15t/generatedstill resolves, but exportsundefined.- The fetch failed in
next dev, or in a build withC15T_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 or Pages Router guide:
- Load the page twice. A warm manifest setup resolves policy without calling
the upstream backend
/init. A browser request to your local/api/c15t/initis expected when client initialization is needed. - Make a consent choice. Confirm the submission reaches the backend
/subjectsendpoint and reload to check that the choice persists. - 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.
- 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.
- 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:
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.tsexportswithConsentManifest(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 devafter 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:
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
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. 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.
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.
'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
IABProviderwithIABConsentBannerandIABConsentDialogfromc15t/react/iabinside your consent provider. They can sit next to the standard banner. - If it does not, remove the
iabmodel 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.
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 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.