Skip to main content

JavaScript Verify and troubleshoot

Troubleshooting

Check. Each vendor needs one owner. Look for a snippet left in index.html, a second init() call, or a script loader attached to a kernel that @c15t/browser or createConsentRuntime already owns. Each of these loads vendors outside the consent gate or twice.

Fix. Call init() or create the runtime once per page, in the browser entry point, and pass every vendor through its scripts list.

Why does the page reload when a visitor saves?

Check. Did the visitor turn off a category they had allowed? @c15t/browser and createConsentRuntime reload the page then, because code that already ran cannot be unloaded. Allowing categories never reloads.

Fix. To handle withdrawal yourself, pass reloadOnConsentRevoked: false, and stop each vendor with its own opt-out call. See scripts.

Why is my custom banner empty?

Check. Confirm that you subscribed before calling runtime.start(), that you wait for resolution.status to be 'matched', and that you render promptRequirement.kind 'notice' as well as 'choice'.

Fix. Render the headless UI from the snapshot, not from assumptions. Headless lists what a custom UI must cover.

Why does the build fail to download the manifest?

Check. vite build stops with an error from consentManifest that starts with the plugin's name, such as @c15t/core/build: could not fetch the consent manifest from <url> during the build. The name is @c15t/core/build for c15t/build and @c15t/vue/vite for c15t/vue/vite. vite dev logs the same message as a warning and keeps going. The part in parentheses names the cause:

MessageCauseFix
fetch failed, with ENOTFOUND, ECONNREFUSED or another network errorThe build cannot reach the backendBuild where the backend is reachable
no response within 10 secondsThe backend did not answer in timeCheck the backend, or build where it is reachable
/manifest responded 404The URL is not your project's backend, such as the https://your-project.inth.app placeholder or a URL missing its path prefixCopy the backend URL exactly as Inth shows it, or set VITE_C15T_BACKEND_URL
/manifest returned an invalid consent manifest.The URL answered with something other than a consent manifest, such as an HTML pagePoint backendURL at the backend itself, not your site or a dashboard page

A failed download never reuses an old snapshot. To deploy while the backend is down, run the build with C15T_ON_BUILD_ERROR=runtime. snapshot is then undefined, and the browser fetches the policy from the backend at runtime.

Only a build that uses manifest() downloads the manifest. The plugin fetches after Vite has dropped the code the app does not use, so a hosted() or offline() build never contacts the backend and never fails this way.

An error or warning that says no backend URL is set means the plugin found no backendURL option and no variable. Set VITE_C15T_BACKEND_URL (or VITE_INTH_PROJECT_URL) in .env or the build environment, or pass the absolute backend URL from your Inth project. The troubleshooting guide lists every message.

A notice that the build skipped the consent manifest fetch means the backend URL is relative, such as /api/c15t. Pass the absolute backend URL instead. With onBuildError: 'fail', this stops the build with build-time manifests require an absolute upstream URL.

Why is snapshot undefined?

Check. snapshot from c15t/generated is undefined, and the browser requests ${backendURL}/manifest on page load.

Fix. snapshot is undefined when Vite has no policy to serve:

  • consentManifest is missing from plugins in vite.config.ts. Without the plugin, c15t/generated still resolves, but exports undefined.
  • The fetch failed in vite dev, or in a build with C15T_ON_BUILD_ERROR=runtime. The terminal shows a could not fetch the consent manifest warning.
  • The backend URL is relative, so the plugin skipped the fetch.
  • consentManifest({ source: 'runtime' }) is set, so the plugin never fetches.

Fix the cause, then restart vite dev or rebuild. The plugin fetches once when Vite starts.

Why does /init fail with a CORS or CSP error?

Check. A CORS error means the backend does not trust your app's origin.

Fix. Add the exact origin, including the port in development, to your Inth project's trusted origins.

Check. A CSP error means your policy's connect-src lacks the backend's origin.

Fix. Add the backend's origin to connect-src. See Content Security Policy.

Why does a gated script in my HTML never run?

Check. With the nonce option set, @c15t/browser runs only <script type="text/plain" data-c15t-category> tags that carry the same nonce. Open the console and look for a warning that c15t skipped a data-c15t-category script, and check the tag for data-c15t-activated="untrusted".

Fix. Add the page's nonce to the tag, including tags your code inserts later. See Content Security Policy.

Why is the stock banner unstyled?

Check. Look at your Content Security Policy's style-src. If it does not allow the <style> element the stock UI adds, the policy blocks it.

Fix. Pass the nonce from your style-src as the nonce option, or import @c15t/browser/styles.css with ui: { shadow: false, styles: false } as Content Security Policy describes.

Why does a fetch return status 451?

Check. A network blocker rule matched the request and its category is not allowed. The console logs [c15t] blocked with the rule's ID.

Fix. If the request should not match, check the rule's domain and pathIncludes.

Inspect the active policy

Call this helper with your kernel: consent.kernel from @c15t/browser, runtime.kernel from createConsentRuntime, or one you created. It logs the current state and later changes without recording a choice.

src/observe-consent-policy.ts
import type { ConsentKernel } from 'c15t';

export function observeConsentPolicy(kernel: ConsentKernel) {
	const report = () => {
		const snapshot = kernel.getSnapshot();
		const resolution = snapshot.resolution;

		console.log({
			status: resolution.status,
			policy: resolution.status === 'matched' ? resolution.policy.id : null,
			prompt: snapshot.promptRequirement,
			permissions: snapshot.effectivePermissions,
			location: snapshot.location,
			privacySignals: snapshot.privacySignals,
		});
	};

	report();
	return kernel.subscribe(report);
}

The helper returns an unsubscribe function. Call it when you remove the diagnostic or dispose the app. Production gates should read kernel.getSnapshot().effectivePermissions at the point where an optional feature would run.

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.