> ## Documentation Index
> Fetch the complete documentation index at: https://docs.go.gbgplc.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Troubleshooting

> Diagnose and resolve common GBGBridge Web SDK integration issues.

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.

```typescript theme={null}
// In React
function ErrorDisplay() {
  const host = useBridgeHost();
  return <pre>{host?.lastError ?? "(no errors)"}</pre>;
}

// Or subscribe via the hook
useBridgeHostError((error) => {
  console.error("[Bridge]", error);
});
```

`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.

```typescript theme={null}
for (const message of host?.receivedMessages ?? []) {
  const action = (message.payload as { action: string }).action;
  console.log(`[${message.type}] ${action} — ${message.correlationId}`);
}
```

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.

```typescript theme={null}
for (const req of host?.pendingRequests ?? []) {
  const action = (req.payload as { action: string }).action;
  console.warn(`Unhandled: ${action} (${req.correlationId})`);
}
```

### Delegate-based observability

For continuous logging in development, set a delegate that prints every message:

```typescript theme={null}
host.delegate = {
  onMessage(_host, message) {
    const action = (message.payload as { action: string }).action;
    console.log(`[Bridge in ] ${message.type}: ${action}`, message);
  },
  onMessageSent(_host, message) {
    const action = (message.payload as { action: string }).action;
    console.log(`[Bridge out] ${message.type}: ${action}`, message);
  },
  onUnhandledRequest(_host, request) {
    const action = (request.payload as { action: string }).action;
    console.warn(`[Bridge   ] no handler for: ${action}`);
  },
  onError(_host, error) {
    console.error("[Bridge err]", error);
  },
};
```

### 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).

```javascript theme={null}
// In the iframe console (from DevTools)
console.log(window.location.origin); // verify the iframe's origin matches your allowedOrigins
```

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](/docs/go-v2/developer-integration/sdks/web/concepts#message-envelope).

```typescript theme={null}
// Temporary diagnostic — remove before shipping
window.addEventListener("message", (event) => {
  console.log("raw message", { origin: event.origin, data: event.data });
});
```

***

### 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](/docs/go-v2/developer-integration/sdks/web/concepts#declaring-vs-registering-capabilities).

***

### 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:

```tsx theme={null}
const [iframe, setIframe] = useState<HTMLIFrameElement | null>(null);
// ...
<iframe ref={setIframe} src={journeyUrl} />
```

***

### 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](/docs/go-v2/developer-integration/sdks/web/security#content-security-policy-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:**

```html theme={null}
<iframe src="https://journey.example.com" allow="camera"></iframe>
```

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:

```tsx theme={null}
const allowedOrigins = useMemo(() => [new URL(journeyUrl).origin], [journeyUrl]);
```

***

### 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](/docs/go-v2/developer-integration/sdks/web/embedding#server-side-rendering-nextjs-remix-sveltekit).

***

### 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:

```html theme={null}
<iframe
  src="https://journey.example.com"
  sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
  allow="camera"
></iframe>
```

`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.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:

```
Content-Security-Policy: frame-src https://journey.example.com https://staging.journey.example.com;
```

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:
   ```typescript theme={null}
   window.addEventListener("message", (event) => {
     console.log("raw message", {
       origin: event.origin,
       source: event.source === document.querySelector("iframe")?.contentWindow,
       data: event.data,
     });
   });
   ```
   * 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](/docs/go-v2/developer-integration/sdks/web/faq) — Common questions.
* [Messaging](/docs/go-v2/developer-integration/sdks/web/messaging) — Message flow details.
* [Security](/docs/go-v2/developer-integration/sdks/web/security) — Origin allowlist, CSP, transport.
