{"record":{"id":"6b3062da2cd38a44","repo":"abhigyanpatwari/GitNexus","slug":"job-not-found-6b3062","errorCode":null,"errorMessage":"Job not found","messagePattern":"Job not found","errorType":"http","errorClass":null,"httpStatus":404,"severity":"warning","filePath":"gitnexus/src/server/sse-progress.ts","lineNumber":73,"sourceCode":" *\n * `updateJob` already synthesizes exactly one terminal progress event (and\n * refuses every update once the job is terminal, #2264 P3), and it assigns the\n * status BEFORE emitting — so asking the job is both sufficient and the only\n * source that cannot be spoofed by an intermediate phase label.\n */\nexport const mountSSEProgress = (app: express.Express, routePath: string, jm: JobManager) => {\n  app.get(routePath, (req, res) => {\n    let jobId: string;\n    try {\n      jobId = assertString(req.params.jobId, 'jobId');\n    } catch (err) {\n      const status = err instanceof BadRequestError ? err.status : 400;\n      res.status(status).json({ error: err instanceof Error ? err.message : 'Invalid jobId' });\n      return;\n    }\n    const job = jm.getJob(jobId);\n    if (!job) {\n      res.status(404).json({ error: 'Job not found' });\n      return;\n    }\n\n    let eventId = 0;\n    res.writeHead(200, {\n      'Content-Type': 'text/event-stream',\n      'Cache-Control': 'no-cache',\n      Connection: 'keep-alive',\n      'X-Accel-Buffering': 'no',\n    });\n\n    // Send current state immediately\n    eventId++;\n    res.write(`id: ${eventId}\\ndata: ${JSON.stringify(job.progress)}\\n\\n`);\n\n    // If already terminal, send event and close\n    if (isTerminalJobStatus(job.status)) {\n      eventId++;","sourceCodeStart":55,"sourceCodeEnd":91,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/aac7515d2a8c50a1f8f923c6fb77218b333560d6/gitnexus/src/server/sse-progress.ts#L55-L91","documentation":"HTTP 404 (a JSON body, not an event-stream) returned by the SSE progress endpoints — /api/analyze/:jobId/progress and /api/embed/:jobId/progress, both mounted through mountSSEProgress — when the jobId matches no tracked job. The job lifecycle is the same as the poll endpoints: in-memory records, lost on restart, swept ~1h after terminal state. A non-string jobId parameter instead yields 400 from the assertString check before this point.","triggerScenarios":"Opening an EventSource for an expired (post-TTL) or never-existing job; reconnecting an SSE stream after serve restarted; refreshing a progress page whose URL holds an old jobId; a truncated or typo'd UUID.","commonSituations":"Long-lived progress tabs reconnecting after server upgrades; jobIds kept in URL/session state; EventSource auto-reconnect racing job cleanup.","solutions":["Probe the JSON poll endpoint (GET /api/analyze/:jobId) when the EventSource errors — 404 there too means the record is gone: re-trigger analysis if needed and reconnect with the fresh id","Open the stream immediately after the 202 and close it when a terminal event arrives","Persist repo identity rather than jobIds; re-POST to mint a new id after restarts","Use the full UUID from the 202 response in the URL"],"exampleFix":"// before\nconst es = new EventSource(`/api/analyze/${jobId}/progress`); // errors on 404, no recovery\n\n// after\nconst es = new EventSource(`/api/analyze/${jobId}/progress`);\nes.onerror = () => {\n  fetch(`/api/analyze/${jobId}`).then(async (r) => {\n    if (r.status === 404) {\n      const { jobId: fresh } = await postAnalyze(body); // restart and reconnect\n      reconnect(`/api/analyze/${fresh}/progress`);\n    }\n  });\n};","handlingStrategy":"fallback","validationCode":"// Validate the id before opening the stream\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\nif (!UUID_RE.test(jobId)) throw new Error(`Invalid jobId: ${jobId}`);","typeGuard":"const isJobId = (v: unknown): v is string =>\n  typeof v === 'string' &&\n  /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(v);","tryCatchPattern":"EventSource has no catch — handle onerror: probe the poll endpoint, and on 404 fall back to re-triggering the job and reconnecting with the new id instead of letting the browser retry the dead URL forever.","preventionTips":["Open SSE streams right after receiving the 202","Handle onerror explicitly — a 404 stream never self-heals","Store repo identity, not jobIds, across restarts"],"tags":["http-404","sse","event-stream","job-scheduler","polling","lifecycle"],"backgroundTag":"async-job-not-found","analyzedSha":"aac7515d2a8c50a1f8f923c6fb77218b333560d6","analyzedAt":"2026-08-20T23:29:22.980Z","contentChangedAt":"2026-08-20T23:29:22.980Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}