{"record":{"id":"b115e12cd5519ff8","repo":"overleaf/overleaf","slug":"not-found-client-already-disconnected-res-sendsta","errorCode":null,"errorMessage":"Not Found (client already disconnected res.sendStatus(404))","messagePattern":"Not Found \\(client already disconnected res\\.sendStatus\\(404\\)\\)","errorType":"http","errorClass":null,"httpStatus":404,"severity":"info","filePath":"services/real-time/app/js/HttpApiController.js","lineNumber":57,"sourceCode":"  },\n\n  startDrain(req, res) {\n    const io = req.app.get('io')\n    let rate = req.query.rate || '4'\n    rate = parseFloat(rate) || 0\n    logger.info({ rate }, 'setting client drain rate')\n    DrainManager.startDrain(io, rate)\n    res.sendStatus(204)\n  },\n\n  disconnectClient(req, res, next) {\n    const io = req.app.get('io')\n    const { client_id: clientId } = req.params\n    const client = io.sockets.sockets[clientId]\n\n    if (!client) {\n      logger.debug({ clientId }, 'api: client already disconnected')\n      res.sendStatus(404)\n      return\n    }\n    logger.info({ clientId }, 'api: requesting client disconnect')\n    client.on('disconnect', () => res.sendStatus(204))\n    client.disconnect()\n  },\n}\n","sourceCodeStart":39,"sourceCodeEnd":65,"githubUrl":"https://github.com/overleaf/overleaf/blob/28ad3b03b71cb4311decdcb55c36b33ec10d72db/services/real-time/app/js/HttpApiController.js#L39-L65","documentation":"The disconnect-client API returns HTTP 404 when the requested socket id is not present in io.sockets.sockets, meaning the client has already disconnected (or never existed on this instance). This is an idempotency signal, not a fault in most cases.","triggerScenarios":"Calling disconnect-client with a client_id whose socket already closed, whose id is stale, or whose socket lives on a different real-time instance behind the load balancer.","commonSituations":"Double disconnect requests; disconnecting after the user's tab closed; multi-instance deployments where sticky routing is off and the socket is on another pod; retrying an old client_id after reconnect.","solutions":["Treat 404 as success — the client is already disconnected, which is the desired end state","Verify sticky sessions / direct socket routing if the socket actually exists on another instance","Check for duplicate disconnect calls in the requesting code and deduplicate","If ids are stale, re-fetch the current client id before disconnecting"],"exampleFix":"// before\nawait disconnectClient(clientId) // throws/logs on 404\n// after\nconst res = await disconnectClient(clientId)\nif (res.status === 404) return // already disconnected: OK\nif (!res.ok) throw new Error(`disconnect failed: ${res.status}`)","handlingStrategy":"fallback","validationCode":"// 404 means already disconnected — validate id freshness before calling\nif (!clientId || staleCache.has(clientId)) return skipDisconnect()","typeGuard":null,"tryCatchPattern":"const res = await fetch(disconnectUrl, { method: 'POST' })\nif (res.status === 404) {\n  return // already disconnected: treat as success\n}\nif (!res.ok) throw new Error(`disconnect failed: ${res.status}`)","preventionTips":["Treat 404 as idempotent success, not an error","Deduplicate disconnect calls for the same client id","Use sticky routing in multi-instance deployments so disconnects reach the owning socket"],"tags":["http-404","websocket","idempotency","socket-io"],"backgroundTag":"client-already-disconnected","analyzedSha":"28ad3b03b71cb4311decdcb55c36b33ec10d72db","analyzedAt":"2026-09-03T02:10:22.807Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-10T07:17:11.731Z"}