{"record":{"id":"6e3b171181f17c63","repo":"jamiepine/voicebox","slug":"navigator-api-is-not-available-this-might-be-a-ta","errorCode":null,"errorMessage":"Navigator API is not available. This might be a Tauri configuration issue.","messagePattern":"Navigator API is not available\\. This might be a Tauri configuration issue\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"critical","filePath":"app/src/lib/hooks/useAudioRecording.ts","lineNumber":38,"sourceCode":"  const streamRef = useRef<MediaStream | null>(null);\n  const timerRef = useRef<number | null>(null);\n  const startTimeRef = useRef<number | null>(null);\n  const cancelledRef = useRef<boolean>(false);\n\n  const startRecording = useCallback(async () => {\n    try {\n      setError(null);\n      chunksRef.current = [];\n      cancelledRef.current = false;\n      setDuration(0);\n\n      // Check if getUserMedia is available\n      // In Tauri, navigator.mediaDevices might not be available immediately\n      if (typeof navigator === 'undefined') {\n        const errorMsg =\n          'Navigator API is not available. This might be a Tauri configuration issue.';\n        setError(errorMsg);\n        throw new Error(errorMsg);\n      }\n\n      if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {\n        // Try waiting a bit for Tauri webview to initialize\n        await new Promise((resolve) => setTimeout(resolve, 100));\n\n        if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {\n          console.error('MediaDevices check:', {\n            hasNavigator: typeof navigator !== 'undefined',\n            hasMediaDevices: !!navigator?.mediaDevices,\n            hasGetUserMedia: !!navigator?.mediaDevices?.getUserMedia,\n            isTauri: platform.metadata.isTauri,\n          });\n\n          const errorMsg = platform.metadata.isTauri\n            ? 'Microphone access is not available. Please ensure:\\n1. The app has microphone permissions in System Settings (macOS: System Settings > Privacy & Security > Microphone)\\n2. You restart the app after granting permissions\\n3. You are using Tauri v2 with a webview that supports getUserMedia'\n            : 'Microphone access is not available. Please ensure you are using a secure context (HTTPS or localhost) and that your browser has microphone permissions enabled.';\n          setError(errorMsg);","sourceCodeStart":20,"sourceCodeEnd":56,"githubUrl":"https://github.com/jamiepine/voicebox/blob/51f49dea198384b4eb6087b72c17057c6eb1c1cd/app/src/lib/hooks/useAudioRecording.ts#L20-L56","documentation":"Thrown at the very top of `startRecording()` in `useAudioRecording.ts` (line 34-39) when `typeof navigator === 'undefined'`. In any real browser/Tauri webview the global `navigator` always exists, so hitting this branch means the code is executing outside a client DOM environment — most often during SSR/prerender or in a JS runtime without a DOM.","triggerScenarios":"The hook's `startRecording` runs during server-side rendering or static prerendering (Next.js/React SSR) where `navigator` is undefined; the code is unit-tested in Node without jsdom; the Tauri webview host failed to inject the standard web globals (misconfigured/custom host).","commonSituations":"A component using this hook is rendered on the server without a `'use client'`/`useEffect` guard; a test imports the hook and calls `startRecording` synchronously in a Node runner; an exotic embedded webview strips `navigator`.","solutions":["Only call `startRecording()` from a user gesture inside a client-mounted component (e.g. a button onClick), never during render or module load.","If using SSR, ensure the recording UI is client-only (`'use client'`, or dynamic import with `ssr: false`).","In tests, run under jsdom/happy-dom or mock `navigator`.","Confirm the Tauri webview is a standard WKWebView/WebView2 — a custom host lacking `navigator` is a build/config defect."],"exampleFix":"// before — runs during render/SSR\nconst { startRecording } = useAudioRecording();\nstartRecording();\n\n// after — guard to client + user gesture\nif (typeof navigator === 'undefined') return; // SSR/prerender\n<button onClick={() => startRecording()}>Record</button>","handlingStrategy":"type-guard","validationCode":"// Never call startRecording outside a client-mounted, user-gesture context\nif (typeof navigator === 'undefined') {\n  // SSR/prerender — bail silently instead of throwing\n  return;\n}","typeGuard":"function hasNavigator(): boolean {\n  return typeof navigator !== 'undefined';\n}","tryCatchPattern":"// In the component: only invoke from a gesture, after client mount\n<button disabled={!hasNavigator()} onClick={() => startRecording()}>Record</button>","preventionTips":["Gate recording UI behind a client-only mount (`'use client'`, `useEffect`).","Only call `startRecording()` from a user gesture (onClick/onPointerDown).","In tests, run under jsdom or mock `navigator`."],"tags":["navigator","ssr","tauri","browser-api","voicebox"],"backgroundTag":null,"analyzedSha":"51f49dea198384b4eb6087b72c17057c6eb1c1cd","analyzedAt":"2026-08-12T16:51:42.824Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}