BotID: BotIdClient script never executes on client-rendered pages — requests missing x-is-human

BotID: BotIdClient’s inline script never executes on client-rendered pages — React dev warning + protected requests sent without x-is-human

Package: botid 1.5.11 (latest on npm as of 2026-07-09) Framework: Next.js 16.2.9 (App Router), React 19.2.4 Affected export: BotIdClient from botid/client

Summary

BotIdClient renders its entire client bootstrap as a single raw inline <script dangerouslySetInnerHTML> with no type attribute — from the end of node_modules/botid/dist/client/index.js (1.5.11, verbatim):

var K=({protect:f})=>_jsxruntime.jsx.call(void 0, _jsxruntime.Fragment,{

children:_jsxruntime.jsx.call(void 0, "script",{

dangerouslySetInnerHTML:{__html:`(${L.toString()})({ protect: ${JSON.stringify(f)} });`}

})

});

exports.BotIdClient = K;

(L is the serialized bootstrap that patches window.fetch and XMLHttpRequest so requests to protected routes carry BotID signals, e.g. the x-is-human header.)

This works whenever the HTML is server-rendered: the browser’s parser executes the inline script early, before app code fires any requests. But when the tree is client-rendered — React creates the <script> element itself instead of hydrating parser-created HTML — two things go wrong, because React deliberately neuters script elements it creates (it constructs them via the innerHTML trick, so they are flagged parser-inserted and never run):

  1. Dev-console warning on every such page load. React logs, once per load:

    Encountered a script tag while rendering React component. Scripts inside React components are never executed when rendering on the client. Consider using template tag instead ( <template> HTML content template element - HTML | MDN ).

    React exempts scripts whose type marks them as a data block (e.g. application/ld+json) via its isScriptDataBlock check — but BotID’s script is executable JS, so no type value can opt it out.

  2. Protection is silently absent on those pages. The bootstrap genuinely never executes, so fetch/XHR are never patched. Every call to a protected route from such a page goes out without x-is-human / BotID signals, and server-side checkBotId() behaves as if BotIdClient weren’t mounted at all (that state is what produces its “Possible misconfiguration” server log).

When does client-render happen in practice?

In Next.js App Router, any page delivered via the __next_error__ shell — dev-mode 404s and error pages. The served HTML is just the bare shell (<html id="__next_error__">…) with no SSR’d layout markup, so the entire tree — root layout and BotIdClient included — mounts client-side and hits both failure modes above. The same mechanism applies to any other path where the component mounts without corresponding server-rendered HTML.

Reproduction (~2 minutes)

  1. Fresh Next.js 16 App Router app; npm i botid@1.5.11; mount <BotIdClient protect={[…]} /> in the root layout <head> per the quickstart at BotID .
  2. next dev
  3. Load any non-existent URL (e.g. http://localhost:3000/nope) in the browser → the console shows the React warning quoted above.
  4. curl -s http://localhost:3000/nope | head -c 300 → the response is the <html id="__next_error__"> shell. Note the BotID inline script is absent from the served HTML — confirming React creates it during client render.
  5. On that page, window.fetch is unpatched; any request to a protected route carries no x-is-human header.

Why it matters

  • The warning fires on every dev 404/error-page load and looks like an app bug — we spent a debugging session tracing it through React internals before pinning it on the type-less inline script in botid/client.
  • The substantive half is silent: pages served via the error shell have no client-side BotID at all, and nothing in the browser indicates that.

Suggested fix

Make the component detect non-execution and recover:

  1. Have the inline bootstrap set a marker global as its first statement. (The public types already reserve Window.IS_HUMAN_INITIALIZED, though nothing in the shipped 1.5.11 bootstrap sets it — any dedicated marker works.)
  2. In a useEffect, if the marker is absent — i.e. React created the script element and it never ran — re-inject the same payload via document.createElement('script') + appendChild. DOM-appended scripts execute normally, restoring protection on client-rendered pages, and they never pass through React’s warning path.
  3. Optionally, skip rendering the JSX <script> when mounting in a browser where the marker is absent (a client-render rather than a hydration pass). That removes the dev warning entirely while preserving the parser-executed fast path on server-rendered HTML.

Why we can’t work around it app-side

Conditionally unmounting BotIdClient (e.g. in dev) makes checkBotId() log “Possible misconfiguration” server-side on every protected call, so keeping it mounted unconditionally is the correct configuration — which means the fix has to live in the package.


Environment recap: botid 1.5.11 · next 16.2.9 · react / react-dom 19.2.4 · reproduced 2026-07-09.

Addendum (2026-07-09, after Vercel support’s first reply)

Support pointed out the documented alternative for Next.js ≥ 15.3: initBotId() from botid/client/core in instrumentation-client.ts. We verified this against the shipped 1.5.11 package (the export exists, and the package README indeed recommends it, relegating <BotIdClient/> to “Next.js < 15.3”), and it resolves both symptoms app-side — instrumentation-client runs before hydration, including on error-shell loads, so the fetch/XHR patch installs without React ever creating a script element.

That narrows but does not void this report:

  • <BotIdClient/> remains the documented path for Next.js < 15.3 (and the component is exported without any version gate or warning) — those users still get the silent protection gap on client-rendered pages, plus the dev warning. The marker + appendChild fallback suggested above still applies.
  • Failing silently is the sharp edge: when the inline script doesn’t run, nothing in the browser indicates protection is absent. If fixing the component isn’t planned, a runtime console.warn when it mounts without executing — or deprecating it for ≥ 15.3 in docs and types — would prevent the next team from tracing this through React internals.

There’s another community post with 404 debugging tips that might be helpful. Please give these solutions a try and let us know how it goes.

A human should be around soon to offer more advice. But you can also get helpful information quickly by asking v0.


Possibly related: BotID Deep Analysis flags all requests as isBot: true (unanswered, auto-closed) — a Next.js < 15.3 integration, i.e. the <BotIdClient/> path, where every real user was classified as a bot. That is exactly the classification a site gets anywhere the component’s inline script never executes (no signals → no x-is-human), though a config issue on their side can’t be ruled out from the post alone.

Noting for whoever triages: the reply above is the auto-responder keyword-matching
“404” — this isn’t a deployment 404 and the site serves fine. It’s a bug report
against the botid package: renders its bootstrap as a type-less
inline script, so when React client-renders the tree (e.g. Next’s next_error
shell on dev 404s/error pages) the script never executes and protected requests go
out without x-is-human. Dev 404s are just the trigger condition that exposes it —
nothing to debug on the deployment side. Full repro, root cause, and a suggested
fix are in the OP.

Posted the full report publicly: . Please attach it to the internal
feedback for the BotID team. One more data point: an older unanswered forum report
( BotID Deep Analysis flags all requests as isBot: true ) describes every real user flagged isBot: true on a
Next.js < 15.3 / BotIdClient integration — consistent with the same root cause,
since that’s the classification you’d get when the component’s inline script never
executes.

Hi Baphometnxg,

Your addendum sounds like the right practical path for a Next.js 16 app: avoid <BotIdClient /> and move the client registration into instrumentation-client.ts with initBotId().

For example:

import { initBotId } from "botid/client/core";

initBotId({
  protect: [
    {
      path: "/api/checkout",
      method: "POST",
    },
  ],
});

I’d keep withBotId() in next.config and keep checkBotId() in the server route/action as the actual enforcement step.

For the remaining package-level issue, I think the clearest bug report is exactly the distinction you found:

  • initBotId() in instrumentation-client.ts works for Next.js 15.3+ / 16

  • <BotIdClient /> can fail silently when React client-renders the tree and creates the script element itself

  • the failure mode is not just a dev warning; protected fetch/XHR calls can go out without BotID signals

The acceptance criteria I’d ask for would be either: <BotIdClient /> detects that its bootstrap did not execute and reinjects it outside React, or the component emits a very explicit runtime warning and the docs/types steer Next.js 15.3+ users away from it more strongly.

For your own production app, I would treat the instrumentation-client.ts migration as the fix and not wait on the component behavior unless you still need to support older Next.js versions.