Diagnostic tools
BridgeHost exposes built-in diagnostic surfaces — error state, a message log, and a list of pending requests. Combine them with browser DevTools for the iframe side, and most debugging scenarios resolve quickly.
Observing lastError
The first check ishost.lastError. It captures the most recent error from outbound send failures, delegate exceptions, or handler exceptions.
host.lastError is a snapshot of the most recent error. For every-error visibility (e.g. telemetry), prefer useBridgeHostError or delegate.onError.
Message log
host.receivedMessages contains every validated inbound message (capped at 200; oldest trimmed on overflow). Useful for verifying message flow.
receivedMessages is mutated in place — React won’t re-render on changes. Read it imperatively from inside an event handler, a setInterval for debugging, or mirror to local state via useBridgeHostEvent / delegate.onMessage.
Pending requests
host.pendingRequests shows requests with no registered handler. If this list grows unexpectedly, an action ID is mismatched somewhere — typo, capability ID used as an action ID, or handler registered after the journey already requested it.
Delegate-based observability
For continuous logging in development, set a delegate that prints every message:Browser DevTools
For the iframe side, open browser DevTools and select the iframe’s context from the document picker (Chrome: the<top> dropdown in Console; Firefox: the iframe selector in the Inspector).
window.addEventListener("message", ...) on the host-page console to see raw events before the SDK validates them — useful when inbound messages are being silently dropped and you want to confirm they’re arriving at all.
Common issues
Most problems fall into a small set of recurring categories — origin allowlist mismatches, the two-step declare-then-register pattern, lifecycle timing, and message-shape issues.Inbound messages silently dropped
Symptoms: The journey sends messages, buthost.receivedMessages doesn’t grow and delegate.onMessage doesn’t fire.
Possible causes
- Origin mismatch. The most common cause.
event.origin(after normalization) must exactly match an entry inallowedOrigins. Trailing slashes, paths, scheme mismatches, and unexpected port numbers all cause silent drops. - Source mismatch.
event.sourcemust equaliframe.contentWindow. If the iframe was repointed viaiframe.src = ...after the host built, thecontentWindowmay have changed. - Malformed envelope. Missing
version,correlationId,type,timestamp, orpayload.action— ortypenot in{request, response, event}— drops the message before any handler. - Host not yet attached. In React, the provider doesn’t build the host until
iframeis non-null. Messages arriving before that are unreachable.
window.addEventListener("message", ...) to confirm the message is arriving at all. Then check event.origin against your allowedOrigins for exact match. Validate the envelope fields against the schema in Concepts → Message envelope.
Outbound messages silently dropped
Symptoms:host.sendEvent(...) or host.respond(...) is called, but the journey doesn’t receive the message. console.warn from [HostIframeChannel].
Possible causes
iframe.contentWindowis null. The iframe was removed from the DOM or never loaded.iframe.srcwas empty at send time. The channel resolvestargetOriginfromiframe.srcon every send. If empty, the send is dropped.- Resolved
targetOriginnot inallowedOrigins. Ififrame.srchas been repointed to a host outside the allowlist, outbound sends are dropped to prevent leakage.
console.warn — it tells you which condition tripped. Verify iframe.src is set before the first outbound send, and that its origin is in the allowlist.
Capability query returns the wrong shape
Symptoms: The journey’sbridge.hasCapability(...) returns false even though your handler is registered. Or the response is missing capabilities you expect.
Possible causes
- Forgot the
capabilitiesprop. This is the #1 web integration mistake.useBridgeCapabilityandregisterHandleronly register handlers — they do NOT declare capabilities. Pass an entry incapabilities(provider prop orBridgeHostOptions), or useregisterCustomCapability(id, version, handler)to do both in one call. - Capability declared but no handler registered. The capability appears in the response as supported, but the journey gets
pendingRequestswhen it tries to invoke. Different problem, opposite symptom. - Snapshot timing.
BridgeHostProvidersnapshotscapabilitiesat attach time. Adding capabilities to the prop after the host has been built has no effect — useregisterCustomCapabilityfor runtime additions.
Provider’s host stays null
Symptoms: useBridgeHost() returns null forever. Handlers and event listeners never run.
Possible cause
You useduseRef instead of useState for the iframe. Refs are not reactive — the provider’s useEffect watching iframe never sees a change when the iframe element appears.
Fix: Switch to useState<HTMLIFrameElement | null>(null) and pass setIframe as the iframe’s ref callback:
Handler not called for requests
Symptoms: A request appears inhost.receivedMessages and host.pendingRequests but the handler’s handle() method never fires.
Possible causes
- Action ID mismatch. Most common: registered for
"camera.document"(the capability ID) instead of"camera.document.capture"(the action ID). They’re related but distinct strings. - Handler registered after the request arrived. The journey sent the request before the handler component mounted, so it landed in
pendingRequests. Subsequent identical actions will route correctly once the handler is registered, but the original pending entry needs a manualhost.respond(...). - Two handler components racing. If two components both call
useBridgeCapability("...")for the same action, the later mount overwrites the earlier. Verify only one handler is active per action.
host.pendingRequests to see what action string was actually sent, then verify your registration uses the same string.
CSP errors / blank iframe
Symptoms: Iframe is empty. Browser console showsRefused to frame ... because it violates the Content Security Policy directive.
Possible causes
frame-srcdoesn’t include the journey origin.frame-srcnot specified at all (some CSP configurations default to'none').- Mixed content — host is HTTPS, journey URL is HTTP.
Content-Security-Policy: frame-src https://journey.example.com;. See Security → CSP.
Camera permission not delegated to the iframe
Symptoms: Inside the iframe,getUserMedia fails with NotAllowedError even though the host has camera permission.
Cause: The iframe allow attribute is missing or doesn’t include camera.
Fix:
allow="camera; microphone".
Large payloads cause slowness or memory pressure
Symptoms: Slow message round-trips, browser tab unresponsive, memory warnings during capture flows. Cause: Multi-megabyte base64 image strings stresspostMessage serialization and the journey’s downstream upload.
Fix: Capture at moderate resolution (1280×720 or 1920×1080 is typical). Use JPEG with quality ~85 rather than PNG when the image is photographic. If you absolutely need full-resolution images, consider uploading directly from the host to your backend and passing a URL through the bridge instead of the image data.
Provider rebuilds repeatedly
Symptoms:useEffect cleanup logs (or React DevTools profiler) show the provider unmounting and remounting on every parent render, breaking the bridge connection.
Possible cause
Theiframe prop receives a new value on every render because the parent uses an inline element creation pattern, or key is being computed dynamically and changing on every render.
Fix: Track the iframe in state (useState), don’t use inline computations for key, and memoize allowedOrigins if it’s computed:
SSR / hydration errors
Symptoms:ReferenceError: window is not defined, or hydration mismatch errors in Next.js / Remix / SvelteKit.
Cause: BridgeHost accesses window, MessageEvent, and the DOM — none of which exist on the server.
Fix: Mark the bridge component as client-only:
- Next.js App Router:
"use client"directive at the top of the component file. - Next.js Pages Router: wrap in
dynamic(() => import("./Journey"), { ssr: false }). - Nuxt / SvelteKit: use the framework’s client-only gate (
<ClientOnly>,onMount, etc.).
Iframe sandbox attribute strips required capabilities
Symptoms: The iframe loads but the journey can’t access camera, can’t callpostMessage, or fails to load its own scripts.
Cause: The iframe was rendered with a restrictive sandbox attribute. <iframe sandbox> (no value) disables almost everything — scripts, same-origin storage, popups, top-navigation, forms. Each sandbox="..." token re-enables one feature, but you have to opt back in explicitly.
Fix: Either drop the sandbox attribute entirely, or include the tokens the journey needs:
allow-scripts and allow-same-origin together are the minimum for any modern journey. Without allow-same-origin, the journey iframe is treated as a unique opaque origin — your allowedOrigins allowlist will never match. Most identity journey integrations omit the sandbox attribute entirely and rely on CSP + allow for isolation instead.
Third-party storage / cookie blocking
Symptoms: The journey loads but immediately fails authentication, loses session state on page reload, or shows “this content is blocked” warnings in Safari. Cause: Browser tracking-prevention features (Safari’s Intelligent Tracking Prevention, Firefox’s Total Cookie Protection) restrict third-party cookies,localStorage, and IndexedDB when the iframe origin differs from the host page’s eTLD+1. The journey’s session storage may be silently isolated or blocked.
Fix:
- Use the Storage Access API in the journey side if it depends on cross-site storage (this is journey-side work, not host-side — coordinate with the journey team).
- Serve the journey from a subdomain of your host (
journey.your-app.comrather thanjourney.gbg.example.com) where possible — browsers treat same-eTLD+1 storage less aggressively. - Avoid testing in Safari Private Browsing during development unless you’re specifically validating private-mode behaviour — Safari’s storage isolation there is even stricter.
CSP violation in browser DevTools
Symptoms: Console showsRefused to frame 'https://...' because it violates the following Content Security Policy directive: "frame-src 'none'" (or similar).
Cause: CSP frame-src (or default-src falling through) doesn’t include the journey origin.
Fix: Add the journey origin to your Content-Security-Policy response header. For multiple environments, list each:
Content-Security-Policy response header. If it doesn’t include the journey origin in frame-src, that’s your culprit.
If your CSP is set via a <meta http-equiv="Content-Security-Policy"> tag in HTML, the same rules apply but you’ll find it in the document source rather than headers.
Mixed content blocking
Symptoms: Iframe is empty. DevTools console showsMixed Content: The page at 'https://...' was loaded over HTTPS, but requested an insecure frame 'http://...'. This request has been blocked.
Cause: Your host page is HTTPS but the journey URL is HTTP. Modern browsers block this silently in production.
Fix: Serve the journey over HTTPS. If you absolutely need HTTP for local dev (rare — localhost is treated as secure by browsers), test from http://localhost host pages, not HTTPS-but-self-signed.
postMessage origin mismatch — diagnostic walkthrough
Symptoms: The journey sends messages buthost.receivedMessages stays empty. delegate.onMessage never fires. No console errors.
Diagnostic steps (each rules out one cause):
-
Attach a raw listener before the SDK initializes:
- If raw listener fires → SDK is dropping; jump to step 2.
- If raw listener never fires → journey isn’t sending; coordinate with journey team.
-
Compare
event.originto yourallowedOriginsentries character by character. Common mismatches:- Trailing slash in your entry (
"https://journey.example.com/"vs incoming"https://journey.example.com") - Path component (
"https://journey.example.com/start"is not a valid origin) - Wrong scheme (
http://vshttps://) - Non-default port not declared (
https://journey.example.com:8443requires the explicit port) - Subdomain typo (
stagging.vsstaging.)
- Trailing slash in your entry (
-
Check
event.sourceagainstiframe.contentWindow. If the iframe was repointed viaiframe.src = ...after the host built, thecontentWindowreference may have changed. -
Validate the envelope shape — required fields are
version,correlationId,type(one of"request" | "response" | "event"),timestamp,payload, andpayload.action(non-empty string). Any missing field drops the message.
Debugging checklist
Run through these checks in whichever order matches your symptom — they’re independent, not sequential:- Read
host.lastErrorfor a description. - Inspect
host.receivedMessagesto verify inbound flow. - Inspect
host.pendingRequestsfor action-ID typos. - Set a
delegatethat logsonMessage,onMessageSent,onUnhandledRequest,onErrorfor full visibility. - Add a raw
window.addEventListener("message", ...)to see pre-validation traffic. - Verify
allowedOriginsentries are exact origin strings — no trailing slashes, no paths. - Verify the iframe was tracked via
useState, notuseRef. - Verify the
capabilitiesprop is present (not just handlers). - Verify action IDs match exactly between handler registration and the inbound request’s
payload.action. - Check CSP for
frame-srcand the iframeallowattribute. - Open browser DevTools, select the iframe’s context, and check for runtime errors.
- Confirm
iframe.srcis set before any outbound send fires.