Troubleshooting
If you’re stuck, start here. Most issues are network-related and have a one-line fix.
Cloud mode
”Failed to open session”
The relay URL isn’t reachable. Verify with the Inspector’s Test Relay Connection button (edit mode). For local cloud-relay development, make sure pnpm --filter @pairkit/relay-server dev is running and the URL on PhoneControllerManager points at it (default wss://relay.pairkit.dev; for local dev use ws://localhost:8080).
Phone shows “not found” / can’t reach the relay
If you’re running a local cloud relay, the phone and your computer must be on the same LAN, and the relay URL on the phone needs your machine’s LAN IP — not localhost. Set PUBLIC_ORIGIN=http://<your-lan-ip>:8080 in packages/relay-server/.env and restart the relay so the QR’s join URL uses the LAN IP.
For testing on cellular (phone not on the same network), expose the local relay through a tunnel:
cloudflared tunnel --url http://localhost:8080Set PUBLIC_ORIGIN to whatever URL Cloudflare prints.
Newtonsoft.Json missing
Package Manager → PairKit SDK pulls com.unity.nuget.newtonsoft-json automatically. If it didn’t (offline import, manual UPM tarball, etc.), add it manually: Package Manager → + → Add package by name → com.unity.nuget.newtonsoft-json.
Cached session gives the same code on every run
That’s by design — sessions persist across domain reloads (recompile, Play → Stop → Play) so phones stay connected through iteration. To force a fresh code, click Clear Cached Session in the Inspector (edit mode) or call manager.ClearCachedSession() in code.
SendToPlayer errors with player_not_found
You’re calling it for a player who’s no longer connected. Either the player disconnected (and you didn’t unsubscribe their handler) or you cached a stale playerId. Check manager.Players before sending, or wrap the call in try/catch and treat OnError events as soft failures.
LAN mode
”Could not start LAN relay on port X”
Another process has the port bound, or the OS rejected the bind. Either:
- Change
Lan Portto something else. - Kill the process using the port:
- macOS / Linux:
lsof -i :7777thenkill <pid> - Windows:
netstat -ano | findstr :7777thentaskkill /pid <pid> /f
- macOS / Linux:
Firewall prompt on first run
Expected on Windows Defender / macOS Application Firewall. Allow the Unity process to accept incoming connections. If you mis-clicked Block, both OSes remember the denial — remove Unity from the firewall rules in System Settings and retry.
Phone loads the page but WebSocket fails
The most common cause is AP isolation on the Wi-Fi router. Some guest, corporate, and hotel networks block client-to-client traffic by default. Switch to a home network or your phone’s hotspot to confirm — you can’t fix this in code.
Android 9+ refuses to load the page
Chrome on Android allows cleartext http:// freely, so this shouldn’t happen in practice. If you wrapped the controller in a WebView-based app, you need to enable cleartext traffic in the Android network security config.
iOS prompts for “Local Network” access
Only the iOS host triggers that prompt (for discovery). Controllers on iPhone Safari do not prompt — they just open a URL.
QR shows a LAN IP the phone can’t reach
Happens on multi-NIC machines (VPN, virtualization, multiple Wi-Fi adapters) when Unity picks a non-routable interface. Two fixes:
- Set
Lan IP Overridein the Inspector to your real Wi-Fi IP. - Or open the Inspector URL, copy the join URL, and type it manually on the phone with the correct IP.
Auto-detection lives in LocalRelayServer.GetPrimaryLanIp() if you want to override programmatically.
PlatformNotSupportedException on WebGL
Expected — browsers can’t host listening sockets. Use Cloud mode for WebGL builds. The LocalLanTransport type is [Obsolete(error: true)] on WebGL so any reference fails at compile time as well.
Both modes
OnPlayerLeave fires with Reason: Timeout even though I didn’t disconnect
Either the phone backgrounded the tab (mobile browsers sometimes suspend WebSockets) or the network dropped briefly. The relay’s reconnect-grace window is 30 seconds — if the phone reconnects within that window, no player.leave is fired and OnPlayerJoin doesn’t refire either. If you’re seeing leaves at exactly the 30-second mark, the phone’s network actually went out.
The Inspector shows “Connecting…” forever
Your Relay URL is set to a hostname that doesn’t resolve, or your machine has no internet (Cloud mode), or the LAN port is blocked (LAN mode). For Cloud, run curl https://relay.pairkit.dev/healthz and confirm ok.
Generated protocol C# is out of sync after a shared-protocol change
Re-run the generator:
pnpm --filter @pairkit/shared-protocol generate:csharpThe generated files under sdks/unity/Runtime/Protocol/ are not hand-edited — every change goes through this script.
Still stuck?
Open an issue at github.com/mooumari/PairKit/issues. Include the Unity version, the phone OS + browser, and a paste of the Inspector’s Connection log if visible.