Skip to main content
This guide helps diagnose and resolve common issues when integrating the GBGBridge Web SDK.

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 is host.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.
Note: 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).
You can also 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, but host.receivedMessages doesn’t grow and delegate.onMessage doesn’t fire.

Possible causes

  1. Origin mismatch. The most common cause. event.origin (after normalization) must exactly match an entry in allowedOrigins. Trailing slashes, paths, scheme mismatches, and unexpected port numbers all cause silent drops.
  2. Source mismatch. event.source must equal iframe.contentWindow. If the iframe was repointed via iframe.src = ... after the host built, the contentWindow may have changed.
  3. Malformed envelope. Missing version, correlationId, type, timestamp, or payload.action — or type not in {request, response, event} — drops the message before any handler.
  4. Host not yet attached. In React, the provider doesn’t build the host until iframe is non-null. Messages arriving before that are unreachable.
Fix: Attach a raw 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

  1. iframe.contentWindow is null. The iframe was removed from the DOM or never loaded.
  2. iframe.src was empty at send time. The channel resolves targetOrigin from iframe.src on every send. If empty, the send is dropped.
  3. Resolved targetOrigin not in allowedOrigins. If iframe.src has been repointed to a host outside the allowlist, outbound sends are dropped to prevent leakage.
Fix: Check the console for the SDK’s 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’s bridge.hasCapability(...) returns false even though your handler is registered. Or the response is missing capabilities you expect.

Possible causes

  1. Forgot the capabilities prop. This is the #1 web integration mistake. useBridgeCapability and registerHandler only register handlers — they do NOT declare capabilities. Pass an entry in capabilities (provider prop or BridgeHostOptions), or use registerCustomCapability(id, version, handler) to do both in one call.
  2. Capability declared but no handler registered. The capability appears in the response as supported, but the journey gets pendingRequests when it tries to invoke. Different problem, opposite symptom.
  3. Snapshot timing. BridgeHostProvider snapshots capabilities at attach time. Adding capabilities to the prop after the host has been built has no effect — use registerCustomCapability for runtime additions.
Fix: Confirm both halves of the two-step pattern. See Concepts → Declaring vs. Registering.

Provider’s host stays null

Symptoms: useBridgeHost() returns null forever. Handlers and event listeners never run.

Possible cause

You used useRef 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 in host.receivedMessages and host.pendingRequests but the handler’s handle() method never fires.

Possible causes

  1. 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.
  2. 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 manual host.respond(...).
  3. 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.
Fix: Inspect 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 shows Refused to frame ... because it violates the Content Security Policy directive.

Possible causes

  1. frame-src doesn’t include the journey origin.
  2. frame-src not specified at all (some CSP configurations default to 'none').
  3. Mixed content — host is HTTPS, journey URL is HTTP.
Fix: Add the journey origin to 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:
For selfie flows that need both: 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 stress postMessage 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

The iframe 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.).
See Embedding → Server-side rendering.

Iframe sandbox attribute strips required capabilities

Symptoms: The iframe loads but the journey can’t access camera, can’t call postMessage, 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.
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.com rather than journey.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 shows Refused 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:
To diagnose: open browser DevTools → Network tab → click the host-page document request → headers panel → look for the 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 shows Mixed 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 but host.receivedMessages stays empty. delegate.onMessage never fires. No console errors. Diagnostic steps (each rules out one cause):
  1. 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.
  2. Compare event.origin to your allowedOrigins entries 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:// vs https://)
    • Non-default port not declared (https://journey.example.com:8443 requires the explicit port)
    • Subdomain typo (stagging. vs staging.)
  3. Check event.source against iframe.contentWindow. If the iframe was repointed via iframe.src = ... after the host built, the contentWindow reference may have changed.
  4. Validate the envelope shape — required fields are version, correlationId, type (one of "request" | "response" | "event"), timestamp, payload, and payload.action (non-empty string). Any missing field drops the message.
Stop at the first step that explains the symptom.

Debugging checklist

Run through these checks in whichever order matches your symptom — they’re independent, not sequential:
  • Read host.lastError for a description.
  • Inspect host.receivedMessages to verify inbound flow.
  • Inspect host.pendingRequests for action-ID typos.
  • Set a delegate that logs onMessage, onMessageSent, onUnhandledRequest, onError for full visibility.
  • Add a raw window.addEventListener("message", ...) to see pre-validation traffic.
  • Verify allowedOrigins entries are exact origin strings — no trailing slashes, no paths.
  • Verify the iframe was tracked via useState, not useRef.
  • Verify the capabilities prop is present (not just handlers).
  • Verify action IDs match exactly between handler registration and the inbound request’s payload.action.
  • Check CSP for frame-src and the iframe allow attribute.
  • Open browser DevTools, select the iframe’s context, and check for runtime errors.
  • Confirm iframe.src is set before any outbound send fires.

Next steps

  • FAQ — Common questions.
  • Messaging — Message flow details.
  • Security — Origin allowlist, CSP, transport.