{"record":{"id":"27b4e1403f93a775","repo":"decolua/9router","slug":"mitm-server-is-already-starting-lock-contention","errorCode":null,"errorMessage":"MITM server is already starting (lock contention)","messagePattern":"MITM server is already starting \\(lock contention\\)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"src/mitm/manager.js","lineNumber":504,"sourceCode":"    } catch { /* ignore */ }\n  }\n\n  if (serverProcess && !serverProcess.killed) {\n    throw new Error(\"MITM server is already running\");\n  }\n\n  // Atomically claim lock to prevent concurrent startServer across processes.\n  // O_EXCL (flag: \"wx\") fails with EEXIST if the file already exists.\n  try {\n    fs.writeFileSync(LOCK_FILE, String(process.pid), { flag: \"wx\" });\n  } catch (e) {\n    if (e.code === \"EEXIST\") {\n      let stale = false;\n      try {\n        const pid = parseInt(fs.readFileSync(LOCK_FILE, \"utf-8\").trim(), 10);\n        stale = !pid || !isProcessAlive(pid);\n      } catch { stale = true; } // unreadable lock → treat as stale\n      if (!stale) throw new Error(\"MITM server is already starting (lock contention)\");\n      try { fs.unlinkSync(LOCK_FILE); } catch { /* ignore */ }\n      fs.writeFileSync(LOCK_FILE, String(process.pid), { flag: \"wx\" });\n    } else throw e;\n  }\n\n  try {\n    await killLeftoverMitm(sudoPassword);\n\n  if (!IS_WIN) {\n    const portStatus = await checkPort443Free();\n    if (portStatus === \"in-use\" || portStatus === \"no-permission\") {\n      const owner = await getPort443Owner(sudoPassword);\n      if (owner) {\n        const shortName = owner.name.includes(\"/\")\n          ? owner.name.split(\"/\").filter(Boolean).pop()\n          : owner.name;\n        if (forceKillPort443) {\n          log(`Killing process on port 443 (PID ${owner.pid}, name=${shortName})...`);","sourceCodeStart":486,"sourceCodeEnd":522,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/src/mitm/manager.js#L486-L522","documentation":"9Router's MITM manager serializes concurrent starts across processes with a lock file created atomically via fs.writeFileSync(..., { flag: \"wx\" }) (O_EXCL). When another live process already holds the lock (its PID is readable from the lock file and that PID is still alive), startServer throws 'MITM server is already starting (lock contention)'. Only stale locks (dead PID or unreadable file) are cleaned up and reclaimed.","triggerScenarios":"Calling startServer (or an API/CLI path that starts the MITM proxy) while another 9Router process — dashboard server, CLI instance, or a previous uncrashed spawn — is mid-start and holds the lock; two clients racing the start endpoint simultaneously; a sibling process that wrote the lock then hung before removing it.","commonSituations":"Running `9router` CLI and the dashboard dev server at once and both triggering MITM start; double-clicking a tray/start action firing two starts; a previous start attempt crashed after writing the lock but its PID is still alive (hung process); Docker/multi-instance setups sharing the same MITM_DIR (~/.9router) on one machine.","solutions":["Wait a few seconds and retry — the owning process finishes start and releases the lock; meanwhile check its health endpoint on the MITM port instead of starting again","Find the process holding the lock: read the PID from the lock file under ~/.9router (MITM_DIR) and inspect `ps -p <pid>`; kill it if it is a hung duplicate","Delete the stale-looking lock file manually if the PID is dead or belongs to nothing, then retry start (the code also self-heals stale locks)","Ensure only one 9Router instance runs per machine/DATA_DIR; for multi-instance use separate DATA_DIR values"],"exampleFix":"// before: blind retry loop hammering start\nawait startMitm();\n// after: check if already running before starting\nconst health = await fetch('http://127.0.0.1:<MITM_PORT>/health').then(r => r.ok).catch(() => false);\nif (!health) await startMitm();","handlingStrategy":"retry","validationCode":"const fs = require('fs');\nconst LOCK = require('os').homedir() + '/.9router/mitm/start.lock'; // match MITM_DIR lock path\nfunction lockHolderAlive() {\n  try {\n    const pid = parseInt(fs.readFileSync(LOCK, 'utf-8').trim(), 10);\n    try { process.kill(pid, 0); return pid; } catch { return null; }\n  } catch { return null; }\n}","typeGuard":null,"tryCatchPattern":"const backoff = [500, 1500, 4000];\nfor (const ms of backoff) {\n  try { await startMitm(apiKey); break; }\n  catch (e) {\n    if (!/lock contention/i.test(e.message)) throw e;\n    await new Promise(r => setTimeout(r, ms));\n  }\n}","preventionTips":["Only start the MITM proxy through one entry point (dashboard or CLI), not both","Check the health endpoint first; skip startServer when it already responds","Use a single 9Router instance per DATA_DIR/machine","Treat this error as transient — implement bounded retry with backoff"],"tags":["filesystem","locking","race-condition","mitm"],"backgroundTag":"lock-contention","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}